From bc4c4db574daad76922e9307d947bafad35e8bb2 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Thu, 11 Jan 2024 14:02:46 -0500 Subject: [PATCH 01/45] feat: introduce `WebhookConfig` --- .gitignore | 1 + webhook.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.gitignore b/.gitignore index 9f9a005..3dd43d7 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ report.json # VSCode *.code-workspace .vscode/* +.dev/* \ No newline at end of file diff --git a/webhook.go b/webhook.go index 7121041..65c67f8 100644 --- a/webhook.go +++ b/webhook.go @@ -12,6 +12,8 @@ var ( ErrInvalidInput = fmt.Errorf("invalid input") ) +// Deprecated: This substructure should only be used for backwards compatibility +// matching. Use WebhookConfig instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. type DeliveryConfig struct { // URL is the HTTP URL to deliver messages to. @@ -28,6 +30,53 @@ type DeliveryConfig struct { AlternativeURLs []string `json:"alt_urls,omitempty"` } +// WebhookConfig is a Webhook substructure with data related to event delivery. +type WebhookConfig struct { + // URL is the HTTP URL to deliver messages to. + ReceiverURL string `json:"url"` + + // Accept is content type of outgoing events. The following content types are supported, otherwise + // a 406 response code is returned: application/octet-stream, application/jsonl, application/msgpack. + Accept string `json:"accept"` + + // Secret is the string value. + // (Optional, set to "" to disable behavior). + Secret string `json:"secret,omitempty"` + + // AlternativeURLs is a list of explicit URLs that should be round robin through on failure cases to the main URL. + AlternativeURLs []string `json:"alt_urls,omitempty"` + + // ID is the configured webhook's name used to map hashed events to. + // Refer to the Hash substructure configuration for more details. + ID string `json:"id"` + + // SecretHash is the hash algorithm to be used. Only sha256 HMAC and sha512 HMAC are supported. + // (Optional). + // Deprecated: The Default value is the sha1 HMAC for backwards compatibility. + SecretHash string `json:"secret_hash"` + + // BatchHints is the substructure for configuration related to event batching. + // (Optional, if omited then batches of singal events will be sent) + // Default value will disable batch. All zeros will also disable batch. + BatchHints struct { + // MaxLingerDuration is the maximum delay for batching if MaxMesasges has not been reached. + // Default value will set no maximum value. + MaxLingerDuration time.Duration `json:"max_linger_duration"` + // MaxMesasges is the maximum number of events that will be sent in a single batch. + // Default value will set no maximum value. + MaxMesasges int `json:"max_messages"` + } `json:"batch_hints"` + + // DNSSrvRecord is the substructure for configuration related to load balancing. + DNSSrvRecord struct { + // FQDNs is a list of FQDNs pointing to dns srv records + FQDNs []string `json:"fqdns"` + // LoadBalancingScheme is the scheme to use for load balancing. Either the + // srv record attribute `weight` or `priortiy` can be used. + LoadBalancingScheme string `json:"load_balancing_scheme"` + } `json:"dns_srv_record"` +} + // MetadataMatcherConfig is Webhook substructure with config to match event metadata. type MetadataMatcherConfig struct { // DeviceID is the list of regular expressions to match device id type against. From 41b7f5923abc8f5da533d68571881cc6268aae41 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Thu, 11 Jan 2024 14:08:43 -0500 Subject: [PATCH 02/45] chore: update to WebhookConfig --- webhook.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/webhook.go b/webhook.go index 65c67f8..26ddfb8 100644 --- a/webhook.go +++ b/webhook.go @@ -32,9 +32,6 @@ type DeliveryConfig struct { // WebhookConfig is a Webhook substructure with data related to event delivery. type WebhookConfig struct { - // URL is the HTTP URL to deliver messages to. - ReceiverURL string `json:"url"` - // Accept is content type of outgoing events. The following content types are supported, otherwise // a 406 response code is returned: application/octet-stream, application/jsonl, application/msgpack. Accept string `json:"accept"` @@ -43,16 +40,9 @@ type WebhookConfig struct { // (Optional, set to "" to disable behavior). Secret string `json:"secret,omitempty"` - // AlternativeURLs is a list of explicit URLs that should be round robin through on failure cases to the main URL. - AlternativeURLs []string `json:"alt_urls,omitempty"` - - // ID is the configured webhook's name used to map hashed events to. - // Refer to the Hash substructure configuration for more details. - ID string `json:"id"` - // SecretHash is the hash algorithm to be used. Only sha256 HMAC and sha512 HMAC are supported. // (Optional). - // Deprecated: The Default value is the sha1 HMAC for backwards compatibility. + // The Default value is the sha512 HMAC. SecretHash string `json:"secret_hash"` // BatchHints is the substructure for configuration related to event batching. From c40ff21b42d770419c32226c1bd9cd013f99d75e Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Thu, 11 Jan 2024 17:28:10 -0500 Subject: [PATCH 03/45] Update webhook.go --- webhook.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/webhook.go b/webhook.go index 26ddfb8..dcf5700 100644 --- a/webhook.go +++ b/webhook.go @@ -32,6 +32,10 @@ type DeliveryConfig struct { // WebhookConfig is a Webhook substructure with data related to event delivery. type WebhookConfig struct { + // ID is the configured webhook's name used to map hashed events to. + // Refer to the Hash substructure configuration for more details. + ID string `json:"id"` + // Accept is content type of outgoing events. The following content types are supported, otherwise // a 406 response code is returned: application/octet-stream, application/jsonl, application/msgpack. Accept string `json:"accept"` @@ -77,12 +81,36 @@ type MetadataMatcherConfig struct { // a webhook registration request. The only difference between this struct and // the Webhook struct is the Duration field. type Registration struct { + // CanonicalName is the canonical name of the registration request. + // Reusing a CanonicalName will override the configurations set in that previous + // registration request with the same CanonicalName. + CanonicalName string `json:"canonical_name"` + // Address is the subscription request origin HTTP Address. Address string `json:"registered_from_address"` - // Config contains data to inform how events are delivered. + // Deprecated: This field should only be used for backwards compatibility + // matching. Use ConfigWebhooks instead. + // Config contains data to inform how events are delivered to single url. Config DeliveryConfig `json:"config"` + // Webhooks contains data to inform how events are delivered to multiple urls. + Webhooks []WebhookConfig `json:"webhooks"` + + // Hash is a substructure for configuration related to distributing events among sinks (kafka and webhooks) + Hash struct { + // Field is the wrp field to be used for hashing. + // Either "device_id" or "account" can be used + Field string `json:"field"` + + // FieldRegex is the regular expression to match `Field` type against. + FieldRegex string `json:"field_regex"` + + // IDs is the list of configured webhooks' and kafkas' names that hashed events to be sent to. + // (Optional, if omited all provided `WebhookConfig` and `KafkaConfig` configurations will be used) + IDs []string + } + // FailureURL is the URL used to notify subscribers when they've been cut off due to event overflow. // Optional, set to "" to disable notifications. FailureURL string `json:"failure_url"` From ec3e73a8a1671d83de8508ee603cf48d0adc4567 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Fri, 12 Jan 2024 14:09:53 -0500 Subject: [PATCH 04/45] Update webhook.go --- webhook.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/webhook.go b/webhook.go index dcf5700..9eb9740 100644 --- a/webhook.go +++ b/webhook.go @@ -75,6 +75,15 @@ type WebhookConfig struct { type MetadataMatcherConfig struct { // DeviceID is the list of regular expressions to match device id type against. DeviceID []string `json:"device_id"` + + // Account is the list of regular expressions to match account type against. + Account []string `json:"metadata:/account"` + + // Model is the list of regular expressions to match model type against. + Model []string `json:"metadata:/hw-model"` + + // FirmwareName is the list of regular expressions to match firmware type against. + FirmwareName []string `json:"metadata:/fw-name"` } // Registration is a special struct for unmarshaling a webhook as part of From 604fa5ad33f77b8456d85124bcfdf2dc46431a2c Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Tue, 16 Jan 2024 14:56:54 -0500 Subject: [PATCH 05/45] Update webhook.go --- webhook.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/webhook.go b/webhook.go index 9eb9740..d41ed8d 100644 --- a/webhook.go +++ b/webhook.go @@ -115,9 +115,6 @@ type Registration struct { // FieldRegex is the regular expression to match `Field` type against. FieldRegex string `json:"field_regex"` - // IDs is the list of configured webhooks' and kafkas' names that hashed events to be sent to. - // (Optional, if omited all provided `WebhookConfig` and `KafkaConfig` configurations will be used) - IDs []string } // FailureURL is the URL used to notify subscribers when they've been cut off due to event overflow. From 375a0a7659a0d0221311a3b7f8e2b3c7b4583df3 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Tue, 16 Jan 2024 15:37:48 -0500 Subject: [PATCH 06/45] Update webhook.go --- webhook.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/webhook.go b/webhook.go index d41ed8d..a2617e8 100644 --- a/webhook.go +++ b/webhook.go @@ -71,6 +71,24 @@ type WebhookConfig struct { } `json:"dns_srv_record"` } +// KafkaConfig is a Kafka substructure with data related to event delivery. +type KafkaConfig struct { + // ID is the configured kafka's name used to map hashed events to. + // Refer to the Hash substructure configuration for more details. + ID string `json:"id"` + + // Accept is content type value to set WRP messages to (unless already specified in the WRP). + Accept string `json:"accept"` + + // BootstrapServers is a list of kafka broker addresses. + BootstrapServers []string `json:"bootstrap_servers"` + + // TODO: figure out which kafka configuration substructures we want to expose to users (to be set by users) + // going to be based on https://pkg.go.dev/github.com/IBM/sarama#Config + // this substructures also includes auth related secrets, noted `MaxOpenRequests` will be excluded since it's already exposed + KafkaProducer struct{} `json:"kafka_producer"` +} + // MetadataMatcherConfig is Webhook substructure with config to match event metadata. type MetadataMatcherConfig struct { // DeviceID is the list of regular expressions to match device id type against. @@ -106,6 +124,9 @@ type Registration struct { // Webhooks contains data to inform how events are delivered to multiple urls. Webhooks []WebhookConfig `json:"webhooks"` + // Kafkas contains data to inform how events are delivered to multiple kafkas. + Kafkas []KafkaConfig `json:"kafkas"` + // Hash is a substructure for configuration related to distributing events among sinks (kafka and webhooks) Hash struct { // Field is the wrp field to be used for hashing. @@ -114,7 +135,6 @@ type Registration struct { // FieldRegex is the regular expression to match `Field` type against. FieldRegex string `json:"field_regex"` - } // FailureURL is the URL used to notify subscribers when they've been cut off due to event overflow. From 4a4875f8888b9b405eb4e5843944cb409c9deb52 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Wed, 17 Jan 2024 13:18:19 -0500 Subject: [PATCH 07/45] Update webhook.go --- webhook.go | 159 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 64 deletions(-) diff --git a/webhook.go b/webhook.go index a2617e8..e90d1cf 100644 --- a/webhook.go +++ b/webhook.go @@ -13,7 +13,7 @@ var ( ) // Deprecated: This substructure should only be used for backwards compatibility -// matching. Use WebhookConfig instead. +// matching. Use Webhook instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. type DeliveryConfig struct { // URL is the HTTP URL to deliver messages to. @@ -30,53 +30,82 @@ type DeliveryConfig struct { AlternativeURLs []string `json:"alt_urls,omitempty"` } -// WebhookConfig is a Webhook substructure with data related to event delivery. -type WebhookConfig struct { - // ID is the configured webhook's name used to map hashed events to. - // Refer to the Hash substructure configuration for more details. - ID string `json:"id"` +// MetadataMatcherConfig is Webhook substructure with config to match event metadata. +type MetadataMatcherConfig struct { + // DeviceID is the list of regular expressions to match device id type against. + DeviceID []string `json:"device_id"` +} + +// Deprecated: This structure should only be used for backwards compatibility +// matching. Use RegistrationV2 instead. +// RegistrationV1 is a special struct for unmarshaling a webhook as part of a webhook registration request. +type RegistrationV1 struct { + // Address is the subscription request origin HTTP Address. + Address string `json:"registered_from_address"` + + // Config contains data to inform how events are delivered. + Config DeliveryConfig `json:"config"` + + // FailureURL is the URL used to notify subscribers when they've been cut off due to event overflow. + // Optional, set to "" to disable notifications. + FailureURL string `json:"failure_url"` - // Accept is content type of outgoing events. The following content types are supported, otherwise - // a 406 response code is returned: application/octet-stream, application/jsonl, application/msgpack. + // Events is the list of regular expressions to match an event type against. + Events []string `json:"events"` + + // Matcher type contains values to match against the metadata. + Matcher MetadataMatcherConfig `json:"matcher,omitempty"` + + // Duration describes how long the subscription lasts once added. + Duration CustomDuration `json:"duration"` + + // Until describes the time this subscription expires. + Until time.Time `json:"until"` + + // now is a function that returns the current time. It is used for testing. + nowFunc func() time.Time `json:"-"` +} + +// Webhook is a substructure with data related to event delivery. +type Webhook struct { + // Accept is the encoding type of outgoing events. The following encoding types are supported, otherwise + // a 406 response code is returned: application/octet-stream, application/json, application/jsonl, application/msgpack. + // Note: An `Accept` of application/octet-stream or application/json will result in a single response for batch sizes of 0 or 1 + // and batch sizes greater than 1 will result in a multipart response. An `Accept` of application/jsonl or application/msgpack + // will always result in a single response with a list of batched events for any batch size. Accept string `json:"accept"` + // AcceptEncoding is the content type of outgoing events. The following content types are supported, otherwise + // a 406 response code is returned: gzip. + AcceptEncoding string `json:"accept_encoding"` + // Secret is the string value. // (Optional, set to "" to disable behavior). Secret string `json:"secret,omitempty"` // SecretHash is the hash algorithm to be used. Only sha256 HMAC and sha512 HMAC are supported. // (Optional). - // The Default value is the sha512 HMAC. + // The Default value is the largest sha HMAC supported, sha512 HMAC. SecretHash string `json:"secret_hash"` - // BatchHints is the substructure for configuration related to event batching. - // (Optional, if omited then batches of singal events will be sent) - // Default value will disable batch. All zeros will also disable batch. - BatchHints struct { - // MaxLingerDuration is the maximum delay for batching if MaxMesasges has not been reached. - // Default value will set no maximum value. - MaxLingerDuration time.Duration `json:"max_linger_duration"` - // MaxMesasges is the maximum number of events that will be sent in a single batch. - // Default value will set no maximum value. - MaxMesasges int `json:"max_messages"` - } `json:"batch_hints"` + // ReceiverUrls is the list of receiver urls that will be used where as if the first url fails, + // then the second url would be used and so on. + // Note: either `ReceiverURLs` or `DNSSrvRecord` must be used but not both. + ReceiverURLs []string `json:"receiver_urls"` // DNSSrvRecord is the substructure for configuration related to load balancing. DNSSrvRecord struct { // FQDNs is a list of FQDNs pointing to dns srv records FQDNs []string `json:"fqdns"` + // LoadBalancingScheme is the scheme to use for load balancing. Either the // srv record attribute `weight` or `priortiy` can be used. LoadBalancingScheme string `json:"load_balancing_scheme"` } `json:"dns_srv_record"` } -// KafkaConfig is a Kafka substructure with data related to event delivery. -type KafkaConfig struct { - // ID is the configured kafka's name used to map hashed events to. - // Refer to the Hash substructure configuration for more details. - ID string `json:"id"` - +// Kafka is a substructure with data related to event delivery. +type Kafka struct { // Accept is content type value to set WRP messages to (unless already specified in the WRP). Accept string `json:"accept"` @@ -89,25 +118,25 @@ type KafkaConfig struct { KafkaProducer struct{} `json:"kafka_producer"` } -// MetadataMatcherConfig is Webhook substructure with config to match event metadata. -type MetadataMatcherConfig struct { - // DeviceID is the list of regular expressions to match device id type against. - DeviceID []string `json:"device_id"` - - // Account is the list of regular expressions to match account type against. - Account []string `json:"metadata:/account"` +// FieldRegex is a substructure with data related to regular expressions. +type FieldRegex struct { + // Field is the wrp field to be used for regex. + // All wrp field can be used, refer to the schema for examples. + Field string `json:"field"` - // Model is the list of regular expressions to match model type against. - Model []string `json:"metadata:/hw-model"` - - // FirmwareName is the list of regular expressions to match firmware type against. - FirmwareName []string `json:"metadata:/fw-name"` + // FieldRegex is the regular expression to match `Field` against to. + Regex string `json:"regex"` } -// Registration is a special struct for unmarshaling a webhook as part of -// a webhook registration request. The only difference between this struct and -// the Webhook struct is the Duration field. -type Registration struct { +// RegistrationV2 is a special struct for unmarshaling sink information as part of a sink registration request. +type RegistrationV2 struct { + // ContactInfo contains contact information used to reach the owner of the registration. + ContactInfo struct { + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` + } `json:"contact_info"` + // CanonicalName is the canonical name of the registration request. // Reusing a CanonicalName will override the configurations set in that previous // registration request with the same CanonicalName. @@ -116,36 +145,38 @@ type Registration struct { // Address is the subscription request origin HTTP Address. Address string `json:"registered_from_address"` - // Deprecated: This field should only be used for backwards compatibility - // matching. Use ConfigWebhooks instead. - // Config contains data to inform how events are delivered to single url. - Config DeliveryConfig `json:"config"` - - // Webhooks contains data to inform how events are delivered to multiple urls. - Webhooks []WebhookConfig `json:"webhooks"` + // Sink contains a list of either webhook or kafka registration information, not both. + Sinks struct { + // Webhooks contains data to inform how events are delivered to multiple urls. + Webhooks []Webhook `json:"webhooks"` - // Kafkas contains data to inform how events are delivered to multiple kafkas. - Kafkas []KafkaConfig `json:"kafkas"` + // Kafkas contains data to inform how events are delivered to multiple kafkas. + Kafkas []Kafka `json:"kafkas"` + } `json:"Sinks"` - // Hash is a substructure for configuration related to distributing events among sinks (kafka and webhooks) - Hash struct { - // Field is the wrp field to be used for hashing. - // Either "device_id" or "account" can be used - Field string `json:"field"` + // Hash is a substructure for configuration related to distributing events among sinks. + // Note. Any failures due to a bad regex feild or regex expression will result in a silent failure. + Hash FieldRegex `json:"hash"` - // FieldRegex is the regular expression to match `Field` type against. - FieldRegex string `json:"field_regex"` - } + // BatchHints is the substructure for configuration related to event batching. + // (Optional, if omited then batches of singal events will be sent) + // Default value will disable batch. All zeros will also disable batch. + BatchHints struct { + // MaxLingerDuration is the maximum delay for batching if MaxMesasges has not been reached. + // Default value will set no maximum value. + MaxLingerDuration time.Duration `json:"max_linger_duration"` + // MaxMesasges is the maximum number of events that will be sent in a single batch. + // Default value will set no maximum value. + MaxMesasges int `json:"max_messages"` + } `json:"batch_hints"` // FailureURL is the URL used to notify subscribers when they've been cut off due to event overflow. // Optional, set to "" to disable notifications. FailureURL string `json:"failure_url"` - // Events is the list of regular expressions to match an event type against. - Events []string `json:"events"` - - // Matcher type contains values to match against the metadata. - Matcher MetadataMatcherConfig `json:"matcher,omitempty"` + // Matcher is the list of regular expressions to match incoming events against to. + // Note. Any failures due to a bad regex feild or regex expression will result in a silent failure. + Matcher []FieldRegex `json:"matcher,omitempty"` // Duration describes how long the subscription lasts once added. Duration CustomDuration `json:"duration"` From 879a57eadbe002d7f16fb711b6cc05d8771bb2aa Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Fri, 26 Jan 2024 14:01:59 -0500 Subject: [PATCH 08/45] Update webhook.go --- webhook.go | 64 +++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/webhook.go b/webhook.go index e90d1cf..cbab366 100644 --- a/webhook.go +++ b/webhook.go @@ -61,9 +61,6 @@ type RegistrationV1 struct { // Until describes the time this subscription expires. Until time.Time `json:"until"` - - // now is a function that returns the current time. It is used for testing. - nowFunc func() time.Time `json:"-"` } // Webhook is a substructure with data related to event delivery. @@ -88,12 +85,18 @@ type Webhook struct { // The Default value is the largest sha HMAC supported, sha512 HMAC. SecretHash string `json:"secret_hash"` + // If true, response will use the device content-type and wrp payload as its body + // Otherwise, response will Accecpt as the content-type and wrp message as its body + // Default: False (the entire wrp message is sent) + PayloadOnly bool `json:"payload_only"` + // ReceiverUrls is the list of receiver urls that will be used where as if the first url fails, // then the second url would be used and so on. // Note: either `ReceiverURLs` or `DNSSrvRecord` must be used but not both. ReceiverURLs []string `json:"receiver_urls"` // DNSSrvRecord is the substructure for configuration related to load balancing. + // Note: either `ReceiverURLs` or `DNSSrvRecord` must be used but not both. DNSSrvRecord struct { // FQDNs is a list of FQDNs pointing to dns srv records FQDNs []string `json:"fqdns"` @@ -128,14 +131,26 @@ type FieldRegex struct { Regex string `json:"regex"` } +type BatchHint struct { + // MaxLingerDuration is the maximum delay for batching if MaxMesasges has not been reached. + // Default value will set no maximum value. + MaxLingerDuration time.Duration `json:"max_linger_duration"` + // MaxMesasges is the maximum number of events that will be sent in a single batch. + // Default value will set no maximum value. + MaxMesasges int `json:"max_messages"` +} + +type ContactInfo struct { + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` +} + // RegistrationV2 is a special struct for unmarshaling sink information as part of a sink registration request. type RegistrationV2 struct { // ContactInfo contains contact information used to reach the owner of the registration. - ContactInfo struct { - Name string `json:"name"` - Phone string `json:"phone"` - Email string `json:"email"` - } `json:"contact_info"` + // (Optional). + ContactInfo ContactInfo `json:"contact_info,omitempty"` // CanonicalName is the canonical name of the registration request. // Reusing a CanonicalName will override the configurations set in that previous @@ -145,30 +160,20 @@ type RegistrationV2 struct { // Address is the subscription request origin HTTP Address. Address string `json:"registered_from_address"` - // Sink contains a list of either webhook or kafka registration information, not both. - Sinks struct { - // Webhooks contains data to inform how events are delivered to multiple urls. - Webhooks []Webhook `json:"webhooks"` + // Webhooks contains data to inform how events are delivered to multiple urls. + Webhooks []Webhook `json:"webhooks"` - // Kafkas contains data to inform how events are delivered to multiple kafkas. - Kafkas []Kafka `json:"kafkas"` - } `json:"Sinks"` + // Kafkas contains data to inform how events are delivered to multiple kafkas. + Kafkas []Kafka `json:"kafkas"` // Hash is a substructure for configuration related to distributing events among sinks. // Note. Any failures due to a bad regex feild or regex expression will result in a silent failure. Hash FieldRegex `json:"hash"` - // BatchHints is the substructure for configuration related to event batching. + // BatchHint is the substructure for configuration related to event batching. // (Optional, if omited then batches of singal events will be sent) // Default value will disable batch. All zeros will also disable batch. - BatchHints struct { - // MaxLingerDuration is the maximum delay for batching if MaxMesasges has not been reached. - // Default value will set no maximum value. - MaxLingerDuration time.Duration `json:"max_linger_duration"` - // MaxMesasges is the maximum number of events that will be sent in a single batch. - // Default value will set no maximum value. - MaxMesasges int `json:"max_messages"` - } `json:"batch_hints"` + BatchHint BatchHint `json:"batch_hints"` // FailureURL is the URL used to notify subscribers when they've been cut off due to event overflow. // Optional, set to "" to disable notifications. @@ -178,14 +183,9 @@ type RegistrationV2 struct { // Note. Any failures due to a bad regex feild or regex expression will result in a silent failure. Matcher []FieldRegex `json:"matcher,omitempty"` - // Duration describes how long the subscription lasts once added. - Duration CustomDuration `json:"duration"` - - // Until describes the time this subscription expires. - Until time.Time `json:"until"` - - // now is a function that returns the current time. It is used for testing. - nowFunc func() time.Time `json:"-"` + // Expires describes the time this subscription expires. + // TODO: list of supported formats + Expires time.Time `json:"expires"` } type Option interface { From aea7b5b043d9ce662d07bc8c771ecdcf41b9711e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 19:07:43 +0000 Subject: [PATCH 09/45] chore(deps): bump xmidt-org/shared-go from 4.1.0 to 4.2.0 (#5) Bumps [xmidt-org/shared-go](https://github.com/xmidt-org/shared-go) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/xmidt-org/shared-go/releases) - [Commits](https://github.com/xmidt-org/shared-go/compare/5bc4b83f25ff4c944cd6253ba189e50d1997ab3c...826aa545bb56f6c7c551d44febb420c0293c8bff) --- updated-dependencies: - dependency-name: xmidt-org/shared-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 594a3d0..ebe370d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ on: jobs: ci: - uses: xmidt-org/shared-go/.github/workflows/ci.yml@5bc4b83f25ff4c944cd6253ba189e50d1997ab3c # v4.1.0 + uses: xmidt-org/shared-go/.github/workflows/ci.yml@826aa545bb56f6c7c551d44febb420c0293c8bff # v4.2.0 with: release-type: library secrets: inherit From 4cf5ed4cf86c8f94c294d2e8b44d4d2e1c2e1750 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 19:08:47 +0000 Subject: [PATCH 10/45] chore(deps): bump xmidt-org/shared-go from 4.2.0 to 4.2.2 (#6) Bumps [xmidt-org/shared-go](https://github.com/xmidt-org/shared-go) from 4.2.0 to 4.2.2. - [Release notes](https://github.com/xmidt-org/shared-go/releases) - [Commits](https://github.com/xmidt-org/shared-go/compare/826aa545bb56f6c7c551d44febb420c0293c8bff...9e191cedb6f62d364dc8706c07107c8ef612c346) --- updated-dependencies: - dependency-name: xmidt-org/shared-go dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebe370d..14ce148 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ on: jobs: ci: - uses: xmidt-org/shared-go/.github/workflows/ci.yml@826aa545bb56f6c7c551d44febb420c0293c8bff # v4.2.0 + uses: xmidt-org/shared-go/.github/workflows/ci.yml@9e191cedb6f62d364dc8706c07107c8ef612c346 # v4.2.2 with: release-type: library secrets: inherit From 99b9e0a23dd4dc25c21b954fa8afc40915443f4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 19:39:14 +0000 Subject: [PATCH 11/45] feat(deps): bump github.com/xmidt-org/urlegit from 0.1.0 to 0.1.1 (#7) Bumps [github.com/xmidt-org/urlegit](https://github.com/xmidt-org/urlegit) from 0.1.0 to 0.1.1. - [Release notes](https://github.com/xmidt-org/urlegit/releases) - [Commits](https://github.com/xmidt-org/urlegit/compare/v0.1.0...v0.1.1) --- updated-dependencies: - dependency-name: github.com/xmidt-org/urlegit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8e0b255..fcc8255 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/stretchr/testify v1.8.4 - github.com/xmidt-org/urlegit v0.1.0 + github.com/xmidt-org/urlegit v0.1.1 ) require ( diff --git a/go.sum b/go.sum index 2238b85..ea0ecd9 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/xmidt-org/urlegit v0.1.0 h1:WZLlWo0e5JNZabLEi7/1+sK/np9qrH9XnoB+ZdsHieM= -github.com/xmidt-org/urlegit v0.1.0/go.mod h1:ih/VtgW3xfpV7FNIrHUpNdP0GapcfLOND8y0JwH51vA= +github.com/xmidt-org/urlegit v0.1.1 h1:sjFlckD7Okql7gQACX5hpicqiqD/kIs1hhG7a623dJQ= +github.com/xmidt-org/urlegit v0.1.1/go.mod h1:ih/VtgW3xfpV7FNIrHUpNdP0GapcfLOND8y0JwH51vA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From c2139808aacfd83b783e49000d3b9b31924012b0 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 6 Feb 2024 15:37:19 -0500 Subject: [PATCH 12/45] added structure for retry hints --- webhook.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/webhook.go b/webhook.go index cbab366..b696ccf 100644 --- a/webhook.go +++ b/webhook.go @@ -62,6 +62,13 @@ type RegistrationV1 struct { // Until describes the time this subscription expires. Until time.Time `json:"until"` } +type RetryHint struct { + //RetryEachUrl is the amount of times a URL should be retried given a failed response until the next URL in the request is tried. + RetryEachUrl int `json:"retry_each_url"` + + //MaxRetry is the total amount times a request will be retried. + MaxRetry int `json:"max_retry"` +} // Webhook is a substructure with data related to event delivery. type Webhook struct { @@ -105,6 +112,10 @@ type Webhook struct { // srv record attribute `weight` or `priortiy` can be used. LoadBalancingScheme string `json:"load_balancing_scheme"` } `json:"dns_srv_record"` + + //RetryHint is the substructure for configuration related to retrying requests. + // (Optional, if omited then retries will be based on default values defined by server) + RetryHint RetryHint `json:"retry_hint"` } // Kafka is a substructure with data related to event delivery. @@ -119,6 +130,10 @@ type Kafka struct { // going to be based on https://pkg.go.dev/github.com/IBM/sarama#Config // this substructures also includes auth related secrets, noted `MaxOpenRequests` will be excluded since it's already exposed KafkaProducer struct{} `json:"kafka_producer"` + + //RetryHint is the substructure for configuration related to retrying requests. + // (Optional, if omited then retries will be based on default values defined by server) + RetryHint RetryHint `json:"retry_hint"` } // FieldRegex is a substructure with data related to regular expressions. @@ -186,6 +201,10 @@ type RegistrationV2 struct { // Expires describes the time this subscription expires. // TODO: list of supported formats Expires time.Time `json:"expires"` + + //RetryHint is the substructure for configuration related to retrying requests. + // (Optional, if omited then retries will be based on default values defined by server) + RetryHint RetryHint `json:"retry_hint"` } type Option interface { From 3285ed043d94b619f4c2dd97bf76926b21c8efa0 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 6 Feb 2024 15:43:36 -0500 Subject: [PATCH 13/45] removed the retryhint struct from registration v2 and updated a comment --- webhook.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/webhook.go b/webhook.go index b696ccf..a4d2f4c 100644 --- a/webhook.go +++ b/webhook.go @@ -64,6 +64,7 @@ type RegistrationV1 struct { } type RetryHint struct { //RetryEachUrl is the amount of times a URL should be retried given a failed response until the next URL in the request is tried. + //Default value will be set to none RetryEachUrl int `json:"retry_each_url"` //MaxRetry is the total amount times a request will be retried. @@ -201,10 +202,6 @@ type RegistrationV2 struct { // Expires describes the time this subscription expires. // TODO: list of supported formats Expires time.Time `json:"expires"` - - //RetryHint is the substructure for configuration related to retrying requests. - // (Optional, if omited then retries will be based on default values defined by server) - RetryHint RetryHint `json:"retry_hint"` } type Option interface { From d0621ca5d54509639155e5ab1a59ca212852974b Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Tue, 20 Feb 2024 16:42:24 -0500 Subject: [PATCH 14/45] Update webhook.go --- webhook.go | 1 + 1 file changed, 1 insertion(+) diff --git a/webhook.go b/webhook.go index a4d2f4c..00df611 100644 --- a/webhook.go +++ b/webhook.go @@ -62,6 +62,7 @@ type RegistrationV1 struct { // Until describes the time this subscription expires. Until time.Time `json:"until"` } + type RetryHint struct { //RetryEachUrl is the amount of times a URL should be retried given a failed response until the next URL in the request is tried. //Default value will be set to none From e6d8933cc6cb7e8b50eaa15ddd083f9c32ef56b2 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Thu, 2 May 2024 15:51:25 -0400 Subject: [PATCH 15/45] added register interface and updated validate function --- webhook.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/webhook.go b/webhook.go index 00df611..9cd0ab7 100644 --- a/webhook.go +++ b/webhook.go @@ -12,6 +12,12 @@ var ( ErrInvalidInput = fmt.Errorf("invalid input") ) +type Register interface { + GetId() string + GetPartnerIds() []string + GetUntil() time.Time +} + // Deprecated: This substructure should only be used for backwards compatibility // matching. Use Webhook instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. @@ -207,12 +213,12 @@ type RegistrationV2 struct { type Option interface { fmt.Stringer - Validate(*Registration) error + Validate(Register) error } // Validate is a method on Registration that validates the registration // against a list of options. -func (r *Registration) Validate(opts ...Option) error { +func Validate(r Register, opts ...Option) error { for _, opt := range opts { if opt != nil { if err := opt.Validate(r); err != nil { From 209f0203ea044a3240b247b668592f1e485e749f Mon Sep 17 00:00:00 2001 From: maura fortino Date: Thu, 16 May 2024 13:24:50 -0400 Subject: [PATCH 16/45] moved validation from ancla to webhook and added multi-err functionality --- options.go | 97 ++----- options_test.go | 722 ++++++++++++++++++++++++------------------------ webhook.go | 125 ++++++++- 3 files changed, 509 insertions(+), 435 deletions(-) diff --git a/options.go b/options.go index fcf0571..d0c71dc 100644 --- a/options.go +++ b/options.go @@ -4,8 +4,6 @@ package webhook import ( - "fmt" - "regexp" "time" "github.com/xmidt-org/urlegit" @@ -20,7 +18,7 @@ type errorOption struct { err error } -func (e errorOption) Validate(*Registration) error { +func (e errorOption) Validate(Validator) error { return error(e.err) } @@ -39,9 +37,9 @@ func AtLeastOneEvent() Option { type atLeastOneEventOption struct{} -func (atLeastOneEventOption) Validate(r *Registration) error { - if len(r.Events) == 0 { - return fmt.Errorf("%w: cannot have zero events", ErrInvalidInput) +func (atLeastOneEventOption) Validate(val Validator) error { + if err := val.ValidateOneEvent(); err != nil { + return err } return nil @@ -58,12 +56,9 @@ func EventRegexMustCompile() Option { type eventRegexMustCompileOption struct{} -func (eventRegexMustCompileOption) Validate(r *Registration) error { - for _, e := range r.Events { - _, err := regexp.Compile(e) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) - } +func (eventRegexMustCompileOption) Validate(val Validator) error { + if err := val.ValidateEventRegex(); err != nil { + return err } return nil } @@ -80,12 +75,9 @@ func DeviceIDRegexMustCompile() Option { type deviceIDRegexMustCompileOption struct{} -func (deviceIDRegexMustCompileOption) Validate(r *Registration) error { - for _, e := range r.Matcher.DeviceID { - _, err := regexp.Compile(e) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) - } +func (deviceIDRegexMustCompileOption) Validate(val Validator) error { + if err := val.ValidateDeviceId(); err != nil { + return err } return nil } @@ -107,39 +99,10 @@ type validateRegistrationDurationOption struct { ttl time.Duration } -func (v validateRegistrationDurationOption) Validate(r *Registration) error { - if v.ttl <= 0 { - v.ttl = time.Duration(0) +func (v validateRegistrationDurationOption) Validate(val Validator) error { + if err := val.ValidateDuration(v.ttl); err != nil { + return err } - - if v.ttl != 0 && v.ttl < time.Duration(r.Duration) { - return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) - } - - if r.Until.IsZero() && r.Duration == 0 { - return fmt.Errorf("%w: either Duration or Until must be set", ErrInvalidInput) - } - - if !r.Until.IsZero() && r.Duration != 0 { - return fmt.Errorf("%w: only one of Duration or Until may be set", ErrInvalidInput) - } - - if !r.Until.IsZero() { - nowFunc := time.Now - if r.nowFunc != nil { - nowFunc = r.nowFunc - } - - now := nowFunc() - if v.ttl != 0 && r.Until.After(now.Add(v.ttl)) { - return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) - } - - if r.Until.Before(now) { - return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) - } - } - return nil } @@ -157,7 +120,7 @@ type provideTimeNowFuncOption struct { nowFunc func() time.Time } -func (p provideTimeNowFuncOption) Validate(r *Registration) error { +func (p provideTimeNowFuncOption) Validate(val Validator) error { r.nowFunc = p.nowFunc return nil } @@ -179,15 +142,13 @@ type provideFailureURLValidatorOption struct { checker *urlegit.Checker } -func (p provideFailureURLValidatorOption) Validate(r *Registration) error { +func (p provideFailureURLValidatorOption) Validate(v Validator) error { if p.checker == nil { return nil } - if r.FailureURL != "" { - if err := p.checker.Text(r.FailureURL); err != nil { - return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) - } + if err := v.ValidateFailureURL(p.checker); err != nil { + return err } return nil } @@ -209,16 +170,14 @@ type provideReceiverURLValidatorOption struct { checker *urlegit.Checker } -func (p provideReceiverURLValidatorOption) Validate(r *Registration) error { +func (p provideReceiverURLValidatorOption) Validate(val Validator) error { if p.checker == nil { return nil } - - if r.Config.ReceiverURL != "" { - if err := p.checker.Text(r.Config.ReceiverURL); err != nil { - return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) - } + if err := val.ValidateReceiverURL(p.checker); err != nil { + return err } + return nil } @@ -239,15 +198,13 @@ type provideAlternativeURLValidatorOption struct { checker *urlegit.Checker } -func (p provideAlternativeURLValidatorOption) Validate(r *Registration) error { +func (p provideAlternativeURLValidatorOption) Validate(val Validator) error { if p.checker == nil { return nil } - for _, url := range r.Config.AlternativeURLs { - if err := p.checker.Text(url); err != nil { - return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) - } + if err := val.ValidateAltURL(p.checker); err != nil { + return err } return nil } @@ -266,9 +223,9 @@ func NoUntil() Option { type noUntilOption struct{} -func (noUntilOption) Validate(r *Registration) error { - if !r.Until.IsZero() { - return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) +func (noUntilOption) Validate(val Validator) error { + if err := val.ValidateUntil(); err != nil { + return err } return nil } diff --git a/options_test.go b/options_test.go index e1e0696..90160ad 100644 --- a/options_test.go +++ b/options_test.go @@ -3,383 +3,383 @@ package webhook -import ( - "testing" - "time" +// import ( +// "testing" +// "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/urlegit" -) +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" +// "github.com/xmidt-org/urlegit" +// ) -type optionTest struct { - description string - in Registration - opt Option - opts []Option - str string - expectedErr error -} +// type optionTest struct { +// description string +// in Registration +// opt Option +// opts []Option +// str string +// expectedErr error +// } -func TestErrorOption(t *testing.T) { - run_tests(t, []optionTest{ - { - description: "success", - str: "foo", - }, { - description: "simple error", - opt: Error(ErrInvalidInput), - str: "Error('invalid input')", - expectedErr: ErrInvalidInput, - }, { - description: "simple nil error", - opt: Error(nil), - str: "Error(nil)", - }, - }) -} +// func TestErrorOption(t *testing.T) { +// run_tests(t, []optionTest{ +// { +// description: "success", +// str: "foo", +// }, { +// description: "simple error", +// opt: Error(ErrInvalidInput), +// str: "Error('invalid input')", +// expectedErr: ErrInvalidInput, +// }, { +// description: "simple nil error", +// opt: Error(nil), +// str: "Error(nil)", +// }, +// }) +// } -func TestAtLeastOneEventOption(t *testing.T) { - run_tests(t, []optionTest{ - { - description: "there is an event", - opt: AtLeastOneEvent(), - in: Registration{Events: []string{"foo"}}, - str: "AtLeastOneEvent()", - }, { - description: "multiple events", - opt: AtLeastOneEvent(), - in: Registration{Events: []string{"foo", "bar"}}, - str: "AtLeastOneEvent()", - }, { - description: "there are no events", - opt: AtLeastOneEvent(), - expectedErr: ErrInvalidInput, - }, - }) -} +// func TestAtLeastOneEventOption(t *testing.T) { +// run_tests(t, []optionTest{ +// { +// description: "there is an event", +// opt: AtLeastOneEvent(), +// in: Registration{Events: []string{"foo"}}, +// str: "AtLeastOneEvent()", +// }, { +// description: "multiple events", +// opt: AtLeastOneEvent(), +// in: Registration{Events: []string{"foo", "bar"}}, +// str: "AtLeastOneEvent()", +// }, { +// description: "there are no events", +// opt: AtLeastOneEvent(), +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestEventRegexMustCompile(t *testing.T) { - run_tests(t, []optionTest{ - { - description: "the regex compiles", - opt: EventRegexMustCompile(), - in: Registration{Events: []string{"event.*"}}, - str: "EventRegexMustCompile()", - }, { - description: "multiple events", - opt: EventRegexMustCompile(), - in: Registration{Events: []string{"magic-thing", "event.*"}}, - str: "EventRegexMustCompile()", - }, { - description: "failure", - opt: EventRegexMustCompile(), - in: Registration{Events: []string{"("}}, - expectedErr: ErrInvalidInput, - }, - }) -} +// func TestEventRegexMustCompile(t *testing.T) { +// run_tests(t, []optionTest{ +// { +// description: "the regex compiles", +// opt: EventRegexMustCompile(), +// in: Registration{Events: []string{"event.*"}}, +// str: "EventRegexMustCompile()", +// }, { +// description: "multiple events", +// opt: EventRegexMustCompile(), +// in: Registration{Events: []string{"magic-thing", "event.*"}}, +// str: "EventRegexMustCompile()", +// }, { +// description: "failure", +// opt: EventRegexMustCompile(), +// in: Registration{Events: []string{"("}}, +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestDeviceIDRegexMustCompile(t *testing.T) { - run_tests(t, []optionTest{ - { - description: "the regex compiles", - opt: DeviceIDRegexMustCompile(), - in: Registration{ - Matcher: MetadataMatcherConfig{ - DeviceID: []string{"device.*"}, - }, - }, - str: "DeviceIDRegexMustCompile()", - }, { - description: "multiple device ids", - opt: DeviceIDRegexMustCompile(), - in: Registration{ - Matcher: MetadataMatcherConfig{ - DeviceID: []string{"device.*", "magic-thing"}, - }, - }, - str: "DeviceIDRegexMustCompile()", - }, { - description: "failure", - opt: DeviceIDRegexMustCompile(), - in: Registration{ - Matcher: MetadataMatcherConfig{ - DeviceID: []string{"("}, - }, - }, - expectedErr: ErrInvalidInput, - }, - }) -} +// func TestDeviceIDRegexMustCompile(t *testing.T) { +// run_tests(t, []optionTest{ +// { +// description: "the regex compiles", +// opt: DeviceIDRegexMustCompile(), +// in: Registration{ +// Matcher: MetadataMatcherConfig{ +// DeviceID: []string{"device.*"}, +// }, +// }, +// str: "DeviceIDRegexMustCompile()", +// }, { +// description: "multiple device ids", +// opt: DeviceIDRegexMustCompile(), +// in: Registration{ +// Matcher: MetadataMatcherConfig{ +// DeviceID: []string{"device.*", "magic-thing"}, +// }, +// }, +// str: "DeviceIDRegexMustCompile()", +// }, { +// description: "failure", +// opt: DeviceIDRegexMustCompile(), +// in: Registration{ +// Matcher: MetadataMatcherConfig{ +// DeviceID: []string{"("}, +// }, +// }, +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestValidateRegistrationDuration(t *testing.T) { - now := func() time.Time { - return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) - } - run_tests(t, []optionTest{ - { - description: "success with time in bounds", - opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ - Duration: CustomDuration(4 * time.Minute), - }, - str: "ValidateRegistrationDuration(5m0s)", - }, { - description: "success with time in bounds, exactly", - opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ - Duration: CustomDuration(5 * time.Minute), - }, - }, { - description: "failure with time out of bounds", - opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ - Duration: CustomDuration(6 * time.Minute), - }, - expectedErr: ErrInvalidInput, - }, { - description: "success with max ttl ignored", - opt: ValidateRegistrationDuration(-5 * time.Minute), - in: Registration{ - Duration: CustomDuration(1 * time.Minute), - }, - }, { - description: "success with max ttl ignored, 0 duration", - opt: ValidateRegistrationDuration(0), - in: Registration{ - Duration: CustomDuration(1 * time.Minute), - }, - }, { - description: "success with until in bounds", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(5 * time.Minute), - }, - in: Registration{ - Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), - }, - }, { - description: "failure due to until being before now", - opts: []Option{ - ValidateRegistrationDuration(5 * time.Minute), - ProvideTimeNowFunc(now), - }, - in: Registration{ - Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), - }, - expectedErr: ErrInvalidInput, - }, { - description: "success with until exactly in bounds", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(5 * time.Minute), - }, - in: Registration{ - Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), - }, - }, { - description: "failure due to the options being out of order", - opts: []Option{ - ValidateRegistrationDuration(5 * time.Minute), - ProvideTimeNowFunc(now), - }, - in: Registration{ - Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), - }, - expectedErr: ErrInvalidInput, - }, { - description: "failure with until out of bounds", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(5 * time.Minute), - }, - in: Registration{ - Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), - }, - expectedErr: ErrInvalidInput, - }, { - description: "success with until just needing to be present", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(0), - }, - in: Registration{ - Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), - }, - }, { - description: "failure, both expirations set", - opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ - Duration: CustomDuration(1 * time.Minute), - Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), - }, - expectedErr: ErrInvalidInput, - }, { - description: "failure, no expiration set", - opt: ValidateRegistrationDuration(5 * time.Minute), - expectedErr: ErrInvalidInput, - }, - }) -} +// func TestValidateRegistrationDuration(t *testing.T) { +// now := func() time.Time { +// return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) +// } +// run_tests(t, []optionTest{ +// { +// description: "success with time in bounds", +// opt: ValidateRegistrationDuration(5 * time.Minute), +// in: Registration{ +// Duration: CustomDuration(4 * time.Minute), +// }, +// str: "ValidateRegistrationDuration(5m0s)", +// }, { +// description: "success with time in bounds, exactly", +// opt: ValidateRegistrationDuration(5 * time.Minute), +// in: Registration{ +// Duration: CustomDuration(5 * time.Minute), +// }, +// }, { +// description: "failure with time out of bounds", +// opt: ValidateRegistrationDuration(5 * time.Minute), +// in: Registration{ +// Duration: CustomDuration(6 * time.Minute), +// }, +// expectedErr: ErrInvalidInput, +// }, { +// description: "success with max ttl ignored", +// opt: ValidateRegistrationDuration(-5 * time.Minute), +// in: Registration{ +// Duration: CustomDuration(1 * time.Minute), +// }, +// }, { +// description: "success with max ttl ignored, 0 duration", +// opt: ValidateRegistrationDuration(0), +// in: Registration{ +// Duration: CustomDuration(1 * time.Minute), +// }, +// }, { +// description: "success with until in bounds", +// opts: []Option{ +// ProvideTimeNowFunc(now), +// ValidateRegistrationDuration(5 * time.Minute), +// }, +// in: Registration{ +// Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), +// }, +// }, { +// description: "failure due to until being before now", +// opts: []Option{ +// ValidateRegistrationDuration(5 * time.Minute), +// ProvideTimeNowFunc(now), +// }, +// in: Registration{ +// Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), +// }, +// expectedErr: ErrInvalidInput, +// }, { +// description: "success with until exactly in bounds", +// opts: []Option{ +// ProvideTimeNowFunc(now), +// ValidateRegistrationDuration(5 * time.Minute), +// }, +// in: Registration{ +// Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), +// }, +// }, { +// description: "failure due to the options being out of order", +// opts: []Option{ +// ValidateRegistrationDuration(5 * time.Minute), +// ProvideTimeNowFunc(now), +// }, +// in: Registration{ +// Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), +// }, +// expectedErr: ErrInvalidInput, +// }, { +// description: "failure with until out of bounds", +// opts: []Option{ +// ProvideTimeNowFunc(now), +// ValidateRegistrationDuration(5 * time.Minute), +// }, +// in: Registration{ +// Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), +// }, +// expectedErr: ErrInvalidInput, +// }, { +// description: "success with until just needing to be present", +// opts: []Option{ +// ProvideTimeNowFunc(now), +// ValidateRegistrationDuration(0), +// }, +// in: Registration{ +// Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), +// }, +// }, { +// description: "failure, both expirations set", +// opt: ValidateRegistrationDuration(5 * time.Minute), +// in: Registration{ +// Duration: CustomDuration(1 * time.Minute), +// Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), +// }, +// expectedErr: ErrInvalidInput, +// }, { +// description: "failure, no expiration set", +// opt: ValidateRegistrationDuration(5 * time.Minute), +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestProvideTimeNowFunc(t *testing.T) { - now := func() time.Time { - return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) - } +// func TestProvideTimeNowFunc(t *testing.T) { +// now := func() time.Time { +// return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) +// } - run_tests(t, []optionTest{ - { - description: "success", - opt: ProvideTimeNowFunc(now), - str: "ProvideTimeNowFunc(func)", - }, { - description: "success as nil", - opt: ProvideTimeNowFunc(nil), - str: "ProvideTimeNowFunc(nil)", - }, - }) -} +// run_tests(t, []optionTest{ +// { +// description: "success", +// opt: ProvideTimeNowFunc(now), +// str: "ProvideTimeNowFunc(func)", +// }, { +// description: "success as nil", +// opt: ProvideTimeNowFunc(nil), +// str: "ProvideTimeNowFunc(nil)", +// }, +// }) +// } -func TestProvideFailureURLValidator(t *testing.T) { - checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) - require.NoError(t, err) - require.NotNil(t, checker) +// func TestProvideFailureURLValidator(t *testing.T) { +// checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) +// require.NoError(t, err) +// require.NotNil(t, checker) - run_tests(t, []optionTest{ - { - description: "success, no checker", - opt: ProvideFailureURLValidator(nil), - str: "ProvideFailureURLValidator(nil)", - }, { - description: "success, with checker", - opt: ProvideFailureURLValidator(checker), - in: Registration{ - FailureURL: "https://example.com", - }, - str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", - }, { - description: "failure, with checker", - opt: ProvideFailureURLValidator(checker), - in: Registration{ - FailureURL: "http://example.com", - }, - expectedErr: ErrInvalidInput, - }, - }) -} +// run_tests(t, []optionTest{ +// { +// description: "success, no checker", +// opt: ProvideFailureURLValidator(nil), +// str: "ProvideFailureURLValidator(nil)", +// }, { +// description: "success, with checker", +// opt: ProvideFailureURLValidator(checker), +// in: Registration{ +// FailureURL: "https://example.com", +// }, +// str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", +// }, { +// description: "failure, with checker", +// opt: ProvideFailureURLValidator(checker), +// in: Registration{ +// FailureURL: "http://example.com", +// }, +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestProvideReceiverURLValidator(t *testing.T) { - checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) - require.NoError(t, err) - require.NotNil(t, checker) +// func TestProvideReceiverURLValidator(t *testing.T) { +// checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) +// require.NoError(t, err) +// require.NotNil(t, checker) - run_tests(t, []optionTest{ - { - description: "success, no checker", - opt: ProvideReceiverURLValidator(nil), - str: "ProvideReceiverURLValidator(nil)", - }, { - description: "success, with checker", - opt: ProvideReceiverURLValidator(checker), - in: Registration{ - Config: DeliveryConfig{ - ReceiverURL: "https://example.com", - }, - }, - str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", - }, { - description: "failure, with checker", - opt: ProvideReceiverURLValidator(checker), - in: Registration{ - Config: DeliveryConfig{ - ReceiverURL: "http://example.com", - }, - }, - expectedErr: ErrInvalidInput, - }, - }) -} +// run_tests(t, []optionTest{ +// { +// description: "success, no checker", +// opt: ProvideReceiverURLValidator(nil), +// str: "ProvideReceiverURLValidator(nil)", +// }, { +// description: "success, with checker", +// opt: ProvideReceiverURLValidator(checker), +// in: Registration{ +// Config: DeliveryConfig{ +// ReceiverURL: "https://example.com", +// }, +// }, +// str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", +// }, { +// description: "failure, with checker", +// opt: ProvideReceiverURLValidator(checker), +// in: Registration{ +// Config: DeliveryConfig{ +// ReceiverURL: "http://example.com", +// }, +// }, +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestProvideAlternativeURLValidator(t *testing.T) { - checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) - require.NoError(t, err) - require.NotNil(t, checker) +// func TestProvideAlternativeURLValidator(t *testing.T) { +// checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) +// require.NoError(t, err) +// require.NotNil(t, checker) - run_tests(t, []optionTest{ - { - description: "success, no checker", - opt: ProvideAlternativeURLValidator(nil), - str: "ProvideAlternativeURLValidator(nil)", - }, { - description: "success, with checker", - opt: ProvideAlternativeURLValidator(checker), - in: Registration{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"https://example.com"}, - }, - }, - str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", - }, { - description: "success, with checker and multiple urls", - opt: ProvideAlternativeURLValidator(checker), - in: Registration{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"https://example.com", "https://example.org"}, - }, - }, - str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", - }, { - description: "failure, with checker", - opt: ProvideAlternativeURLValidator(checker), - in: Registration{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"http://example.com"}, - }, - }, - expectedErr: ErrInvalidInput, - }, { - description: "failure, with checker with multiple urls", - opt: ProvideAlternativeURLValidator(checker), - in: Registration{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"https://example.com", "http://example.com"}, - }, - }, - expectedErr: ErrInvalidInput, - }, - }) -} +// run_tests(t, []optionTest{ +// { +// description: "success, no checker", +// opt: ProvideAlternativeURLValidator(nil), +// str: "ProvideAlternativeURLValidator(nil)", +// }, { +// description: "success, with checker", +// opt: ProvideAlternativeURLValidator(checker), +// in: Registration{ +// Config: DeliveryConfig{ +// AlternativeURLs: []string{"https://example.com"}, +// }, +// }, +// str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", +// }, { +// description: "success, with checker and multiple urls", +// opt: ProvideAlternativeURLValidator(checker), +// in: Registration{ +// Config: DeliveryConfig{ +// AlternativeURLs: []string{"https://example.com", "https://example.org"}, +// }, +// }, +// str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", +// }, { +// description: "failure, with checker", +// opt: ProvideAlternativeURLValidator(checker), +// in: Registration{ +// Config: DeliveryConfig{ +// AlternativeURLs: []string{"http://example.com"}, +// }, +// }, +// expectedErr: ErrInvalidInput, +// }, { +// description: "failure, with checker with multiple urls", +// opt: ProvideAlternativeURLValidator(checker), +// in: Registration{ +// Config: DeliveryConfig{ +// AlternativeURLs: []string{"https://example.com", "http://example.com"}, +// }, +// }, +// expectedErr: ErrInvalidInput, +// }, +// }) +// } -func TestNoUntil(t *testing.T) { - run_tests(t, []optionTest{ - { - description: "success, no until set", - opt: NoUntil(), - str: "NoUntil()", - }, { - description: "detect until set", - opt: NoUntil(), - in: Registration{ - Until: time.Now(), - }, - expectedErr: ErrInvalidInput, - }, - }) -} -func run_tests(t *testing.T, tests []optionTest) { - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) +// func TestNoUntil(t *testing.T) { +// run_tests(t, []optionTest{ +// { +// description: "success, no until set", +// opt: NoUntil(), +// str: "NoUntil()", +// }, { +// description: "detect until set", +// opt: NoUntil(), +// in: Registration{ +// Until: time.Now(), +// }, +// expectedErr: ErrInvalidInput, +// }, +// }) +// } +// func run_tests(t *testing.T, tests []optionTest) { +// for _, tc := range tests { +// t.Run(tc.description, func(t *testing.T) { +// assert := assert.New(t) - opts := append(tc.opts, tc.opt) - err := tc.in.Validate(opts...) +// opts := append(tc.opts, tc.opt) +// err := tc.in.Validate(opts...) - assert.ErrorIs(err, tc.expectedErr) +// assert.ErrorIs(err, tc.expectedErr) - if tc.str != "" && tc.opt != nil { - assert.Equal(tc.str, tc.opt.String()) - } - }) - } -} +// if tc.str != "" && tc.opt != nil { +// assert.Equal(tc.str, tc.opt.String()) +// } +// }) +// } +// } diff --git a/webhook.go b/webhook.go index 9cd0ab7..a2c538c 100644 --- a/webhook.go +++ b/webhook.go @@ -4,8 +4,12 @@ package webhook import ( + "errors" "fmt" + "regexp" "time" + + "github.com/xmidt-org/urlegit" ) var ( @@ -18,6 +22,17 @@ type Register interface { GetUntil() time.Time } +type Validator interface { + ValidateOneEvent() error + ValidateEventRegex() error + ValidateDeviceId() error + ValidateUntil() error + ValidateDuration(time.Duration) error + ValidateFailureURL(*urlegit.Checker) error + ValidateReceiverURL(*urlegit.Checker) error + ValidateAltURL(*urlegit.Checker) error +} + // Deprecated: This substructure should only be used for backwards compatibility // matching. Use Webhook instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. @@ -213,18 +228,120 @@ type RegistrationV2 struct { type Option interface { fmt.Stringer - Validate(Register) error + Validate(Validator) error } // Validate is a method on Registration that validates the registration // against a list of options. -func Validate(r Register, opts ...Option) error { +func Validate(v Validator, opts ...Option) error { + var errs error for _, opt := range opts { if opt != nil { - if err := opt.Validate(r); err != nil { - return err + if err := opt.Validate(v); err != nil { + errs = errors.Join(errs, err) } } } + return errs +} + +func (v1 *RegistrationV1) ValidateOneEvent() error { + if len(v1.Events) == 0 { + return fmt.Errorf("%w: cannot have zero events", ErrInvalidInput) + } + return nil +} + +func (v1 *RegistrationV1) ValidateEventRegex() error { + var errs error + for _, e := range v1.Events { + _, err := regexp.Compile(e) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: unable to compile matching", ErrInvalidInput)) + } + } + return errs +} + +func (v1 *RegistrationV1) ValidateDeviceId() error { + var errs error + for _, e := range v1.Matcher.DeviceID { + _, err := regexp.Compile(e) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: unable to compile matching", ErrInvalidInput)) + } + } + return errs +} + +func (v1 *RegistrationV1) ValidateDuration(ttl time.Duration) error { + var errs error + if ttl <= 0 { + ttl = time.Duration(0) + } + + if ttl != 0 && ttl < time.Duration(v1.Duration) { + errs = errors.Join(errs, fmt.Errorf("%w: the registration is for too long", ErrInvalidInput)) + } + + if v1.Until.IsZero() && v1.Duration == 0 { + errs = errors.Join(errs, fmt.Errorf("%w: either Duration or Until must be set", ErrInvalidInput)) + } + + if !v1.Until.IsZero() && v1.Duration != 0 { + errs = errors.Join(errs, fmt.Errorf("%w: only one of Duration or Until may be set", ErrInvalidInput)) + } + + if !v1.Until.IsZero() { + nowFunc := time.Now + // if v1.nowFunc != nil { + // nowFunc = v1.nowFunc + // } + + now := nowFunc() + if ttl != 0 && v1.Until.After(now.Add(ttl)) { + errs = errors.Join(errs, fmt.Errorf("%w: the registration is for too long", ErrInvalidInput)) + } + + if v1.Until.Before(now) { + errs = errors.Join(errs, fmt.Errorf("%w: the registration has already expired", ErrInvalidInput)) + } + } + + return errs +} + +func (v1 *RegistrationV1) ValidateFailureURL(c *urlegit.Checker) error { + if v1.FailureURL != "" { + if err := c.Text(v1.FailureURL); err != nil { + return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) + } + } + return nil +} + +func (v1 *RegistrationV1) ValidateReceiverURL(c *urlegit.Checker) error { + if v1.Config.ReceiverURL != "" { + if err := c.Text(v1.Config.ReceiverURL); err != nil { + return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) + } + } + return nil +} + +func (v1 *RegistrationV1) ValidateAltURL(c *urlegit.Checker) error { + var errs error + for _, url := range v1.Config.AlternativeURLs { + if err := c.Text(url); err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: failure url is invalid", ErrInvalidInput)) + } + } + return errs +} + +func (v1 *RegistrationV1) ValidateUntil() error { + if !v1.Until.IsZero() { + return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) + } return nil } From 1085b01961dd9d478594a95e330ecfce692037be Mon Sep 17 00:00:00 2001 From: maura fortino Date: Thu, 16 May 2024 13:41:25 -0400 Subject: [PATCH 17/45] commenting out nowFunc for now --- options.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/options.go b/options.go index d0c71dc..dc3a411 100644 --- a/options.go +++ b/options.go @@ -112,25 +112,25 @@ func (v validateRegistrationDurationOption) String() string { // ProvideTimeNowFunc is an option that allows the caller to provide a function // that returns the current time. This is used for testing. -func ProvideTimeNowFunc(nowFunc func() time.Time) Option { - return provideTimeNowFuncOption{nowFunc: nowFunc} -} - -type provideTimeNowFuncOption struct { - nowFunc func() time.Time -} - -func (p provideTimeNowFuncOption) Validate(val Validator) error { - r.nowFunc = p.nowFunc - return nil -} - -func (p provideTimeNowFuncOption) String() string { - if p.nowFunc == nil { - return "ProvideTimeNowFunc(nil)" - } - return "ProvideTimeNowFunc(func)" -} +// func ProvideTimeNowFunc(nowFunc func() time.Time) Option { +// return provideTimeNowFuncOption{nowFunc: nowFunc} +// } + +// type provideTimeNowFuncOption struct { +// nowFunc func() time.Time +// } + +// func (p provideTimeNowFuncOption) Validate(val Validator) error { +// r.nowFunc = p.nowFunc +// return nil +// } + +// func (p provideTimeNowFuncOption) String() string { +// if p.nowFunc == nil { +// return "ProvideTimeNowFunc(nil)" +// } +// return "ProvideTimeNowFunc(func)" +// } // ProvideFailureURLValidator is an option that allows the caller to provide a // URL validator that is used to validate the FailureURL. From 47ea530f5d5cddc203525b7b582c09fdcdb1ef75 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Thu, 16 May 2024 15:54:44 -0400 Subject: [PATCH 18/45] removed GetPartnerIds from register and added alwaysvalid as an option for valdiation --- options.go | 14 ++++++++++++++ webhook.go | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/options.go b/options.go index dc3a411..735977d 100644 --- a/options.go +++ b/options.go @@ -29,6 +29,20 @@ func (e errorOption) String() string { return "Error('" + e.err.Error() + "')" } +func AlwaysValid() Option { + return AlwaysValidOption{} +} + +type AlwaysValidOption struct{} + +func (a AlwaysValidOption) Validate(val Validator) error { + return nil +} + +func (a AlwaysValidOption) String() string { + return "alwaysValidOption" +} + // AtLeastOneEvent makes sure there is at least one value in Events and ensures // that all values should parse into regex. func AtLeastOneEvent() Option { diff --git a/webhook.go b/webhook.go index a2c538c..5dad91e 100644 --- a/webhook.go +++ b/webhook.go @@ -18,7 +18,6 @@ var ( type Register interface { GetId() string - GetPartnerIds() []string GetUntil() time.Time } From 58b841f23325d641828b2f18ae9d4ff942c795db Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 28 May 2024 11:42:59 -0400 Subject: [PATCH 19/45] added validation file and moved build validators from ancla to this package --- validation.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ webhook.go | 11 ------ 2 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 validation.go diff --git a/validation.go b/validation.go new file mode 100644 index 0000000..9c033d6 --- /dev/null +++ b/validation.go @@ -0,0 +1,102 @@ +package webhook + +import ( + "time" + + "github.com/xmidt-org/urlegit" +) + +type Validator interface { + ValidateOneEvent() error + ValidateEventRegex() error + ValidateDeviceId() error + ValidateUntil() error + ValidateDuration(time.Duration) error + ValidateFailureURL(*urlegit.Checker) error + ValidateReceiverURL(*urlegit.Checker) error + ValidateAltURL(*urlegit.Checker) error +} + +type ValidatorConfig struct { + URL URLVConfig + TTL TTLVConfig +} + +type URLVConfig struct { + HTTPSOnly bool + AllowLoopback bool + AllowIP bool + AllowSpecialUseHosts bool + AllowSpecialUseIPs bool + InvalidHosts []string + InvalidSubnets []string +} + +type TTLVConfig struct { + Max time.Duration + Jitter time.Duration + Now func() time.Time +} + +var ( + SpecialUseIPs = []string{ + "0.0.0.0/8", //local ipv4 + "fe80::/10", //local ipv6 + "255.255.255.255/32", //broadcast to neighbors + "2001::/32", //ipv6 TEREDO prefix + "2001:5::/32", //EID space for lisp + "2002::/16", //ipv6 6to4 + "fc00::/7", //ipv6 unique local + "192.0.0.0/24", //ipv4 IANA + "2001:0000::/23", //ipv6 IANA + "224.0.0.1/32", //ipv4 multicast + } + // errFailedToBuildValidators = errors.New("failed to build validators") + // errFailedToBuildValidURLFuncs = errors.New("failed to build ValidURLFuncs") +) + +// BuildURLChecker translates the configuration into url Checker to be run on the webhook. +func buildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { + var o []urlegit.Option + if config.URL.HTTPSOnly { + o = append(o, urlegit.OnlyAllowSchemes("https")) + } + if !config.URL.AllowLoopback { + o = append(o, urlegit.ForbidLoopback()) + } + if !config.URL.AllowIP { + o = append(o, urlegit.ForbidAnyIPs()) + } + if !config.URL.AllowSpecialUseHosts { + o = append(o, urlegit.ForbidSpecialUseDomains()) + } + if !config.URL.AllowSpecialUseIPs { + o = append(o, urlegit.ForbidSubnets(SpecialUseIPs)) + } + checker, err := urlegit.New(o...) + if err != nil { + return nil, err + } + return checker, nil +} + +// BuildValidators translates the configuration into a list of validators to be run on the +// webhook. +func BuildValidators(config ValidatorConfig) ([]Option, error) { + var opts []Option + + checker, err := buildURLChecker(config) + if err != nil { + return nil, err + } + opts = append(opts, + AtLeastOneEvent(), + EventRegexMustCompile(), + DeviceIDRegexMustCompile(), + ValidateRegistrationDuration(config.TTL.Max), + ProvideReceiverURLValidator(checker), + ProvideFailureURLValidator(checker), + ProvideAlternativeURLValidator(checker), + ) + return opts, nil +} diff --git a/webhook.go b/webhook.go index 5dad91e..1301fd9 100644 --- a/webhook.go +++ b/webhook.go @@ -21,17 +21,6 @@ type Register interface { GetUntil() time.Time } -type Validator interface { - ValidateOneEvent() error - ValidateEventRegex() error - ValidateDeviceId() error - ValidateUntil() error - ValidateDuration(time.Duration) error - ValidateFailureURL(*urlegit.Checker) error - ValidateReceiverURL(*urlegit.Checker) error - ValidateAltURL(*urlegit.Checker) error -} - // Deprecated: This substructure should only be used for backwards compatibility // matching. Use Webhook instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. From 1bca26ebfaca0144ba700b9abd6d8149d0504beb Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 29 May 2024 09:22:59 -0400 Subject: [PATCH 20/45] added validation for until --- options.go | 18 +++++++++++++++++- validation.go | 1 + webhook.go | 27 +++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/options.go b/options.go index 735977d..2bdb1d8 100644 --- a/options.go +++ b/options.go @@ -238,7 +238,7 @@ func NoUntil() Option { type noUntilOption struct{} func (noUntilOption) Validate(val Validator) error { - if err := val.ValidateUntil(); err != nil { + if err := val.ValidateNoUntil(); err != nil { return err } return nil @@ -247,3 +247,19 @@ func (noUntilOption) Validate(val Validator) error { func (noUntilOption) String() string { return "NoUntil()" } + +func Until() Option { + return untilOption{} +} + +type untilOption struct{} + +func (untilOption) Validate(val Validator) error { + if err := val.ValidateUntil(); err != nil { + return err + } + return nil +} +func (untilOption) String() string { + return "Until()" +} diff --git a/validation.go b/validation.go index 9c033d6..84f937a 100644 --- a/validation.go +++ b/validation.go @@ -11,6 +11,7 @@ type Validator interface { ValidateEventRegex() error ValidateDeviceId() error ValidateUntil() error + ValidateNoUntil() error ValidateDuration(time.Duration) error ValidateFailureURL(*urlegit.Checker) error ValidateReceiverURL(*urlegit.Checker) error diff --git a/webhook.go b/webhook.go index 1301fd9..bb7a9ea 100644 --- a/webhook.go +++ b/webhook.go @@ -221,7 +221,7 @@ type Option interface { // Validate is a method on Registration that validates the registration // against a list of options. -func Validate(v Validator, opts ...Option) error { +func Validate(v Validator, opts []Option) error { var errs error for _, opt := range opts { if opt != nil { @@ -327,9 +327,32 @@ func (v1 *RegistrationV1) ValidateAltURL(c *urlegit.Checker) error { return errs } -func (v1 *RegistrationV1) ValidateUntil() error { +func (v1 *RegistrationV1) ValidateNoUntil() error { if !v1.Until.IsZero() { return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) } return nil } + +func (v1 *RegistrationV1) ValidateUntil(jitter time.Duration, maxTTL time.Duration, now func() time.Time) error { + if now == nil { + now = time.Now + } + if maxTTL < 0 { + return ErrInvalidInput + } else if jitter < 0 { + return ErrInvalidInput + } + + if v1.Until.IsZero() { + return nil + } + limit := (now().Add(maxTTL)).Add(jitter) + proposed := (v1.Until) + if proposed.After(limit) { + return fmt.Errorf("%w: %v after %v", + ErrInvalidInput, proposed.String(), limit.String()) + } + return nil + +} From d30c20fb0fe4166a5ee94fd4fa2693599425a95d Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 29 May 2024 09:59:44 -0400 Subject: [PATCH 21/45] added back in nowFunc to registrationv1 and added validation for it --- options.go | 38 +++++++++++++++++++------------------- validation.go | 1 + webhook.go | 13 ++++++++++--- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/options.go b/options.go index 2bdb1d8..e522ac1 100644 --- a/options.go +++ b/options.go @@ -126,25 +126,25 @@ func (v validateRegistrationDurationOption) String() string { // ProvideTimeNowFunc is an option that allows the caller to provide a function // that returns the current time. This is used for testing. -// func ProvideTimeNowFunc(nowFunc func() time.Time) Option { -// return provideTimeNowFuncOption{nowFunc: nowFunc} -// } - -// type provideTimeNowFuncOption struct { -// nowFunc func() time.Time -// } - -// func (p provideTimeNowFuncOption) Validate(val Validator) error { -// r.nowFunc = p.nowFunc -// return nil -// } - -// func (p provideTimeNowFuncOption) String() string { -// if p.nowFunc == nil { -// return "ProvideTimeNowFunc(nil)" -// } -// return "ProvideTimeNowFunc(func)" -// } +func ProvideTimeNowFunc(nowFunc func() time.Time) Option { + return provideTimeNowFuncOption{nowFunc: nowFunc} +} + +type provideTimeNowFuncOption struct { + nowFunc func() time.Time +} + +func (p provideTimeNowFuncOption) Validate(val Validator) error { + val.SetNowFunc(p.nowFunc) + return nil +} + +func (p provideTimeNowFuncOption) String() string { + if p.nowFunc == nil { + return "ProvideTimeNowFunc(nil)" + } + return "ProvideTimeNowFunc(func)" +} // ProvideFailureURLValidator is an option that allows the caller to provide a // URL validator that is used to validate the FailureURL. diff --git a/validation.go b/validation.go index 84f937a..775a336 100644 --- a/validation.go +++ b/validation.go @@ -16,6 +16,7 @@ type Validator interface { ValidateFailureURL(*urlegit.Checker) error ValidateReceiverURL(*urlegit.Checker) error ValidateAltURL(*urlegit.Checker) error + SetNowFunc(func() time.Time) } type ValidatorConfig struct { diff --git a/webhook.go b/webhook.go index bb7a9ea..b1f9c9c 100644 --- a/webhook.go +++ b/webhook.go @@ -70,6 +70,9 @@ type RegistrationV1 struct { // Until describes the time this subscription expires. Until time.Time `json:"until"` + + // now is a function that returns the current time. It is used for testing. + nowFunc func() time.Time `json:"-"` } type RetryHint struct { @@ -282,9 +285,9 @@ func (v1 *RegistrationV1) ValidateDuration(ttl time.Duration) error { if !v1.Until.IsZero() { nowFunc := time.Now - // if v1.nowFunc != nil { - // nowFunc = v1.nowFunc - // } + if v1.nowFunc != nil { + nowFunc = v1.nowFunc + } now := nowFunc() if ttl != 0 && v1.Until.After(now.Add(ttl)) { @@ -356,3 +359,7 @@ func (v1 *RegistrationV1) ValidateUntil(jitter time.Duration, maxTTL time.Durati return nil } + +func (v1 *RegistrationV1) SetNowFunc(now func() time.Time) { + v1.nowFunc = now +} From 8cd830206517a65c4ac013074c851d49e82a9f3f Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 29 May 2024 10:09:25 -0400 Subject: [PATCH 22/45] updatedthe until option validation --- options.go | 18 +++++++++++++----- validation.go | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/options.go b/options.go index e522ac1..9a6423b 100644 --- a/options.go +++ b/options.go @@ -248,14 +248,22 @@ func (noUntilOption) String() string { return "NoUntil()" } -func Until() Option { - return untilOption{} +func Until(j time.Duration, m time.Duration, now func() time.Time) Option { + return untilOption{ + jitter: j, + max: m, + now: now, + } } -type untilOption struct{} +type untilOption struct { + jitter time.Duration + max time.Duration + now func() time.Time +} -func (untilOption) Validate(val Validator) error { - if err := val.ValidateUntil(); err != nil { +func (u untilOption) Validate(val Validator) error { + if err := val.ValidateUntil(u.jitter, u.max, u.now); err != nil { return err } return nil diff --git a/validation.go b/validation.go index 775a336..48db5fc 100644 --- a/validation.go +++ b/validation.go @@ -10,7 +10,7 @@ type Validator interface { ValidateOneEvent() error ValidateEventRegex() error ValidateDeviceId() error - ValidateUntil() error + ValidateUntil(time.Duration, time.Duration, func() time.Time) error ValidateNoUntil() error ValidateDuration(time.Duration) error ValidateFailureURL(*urlegit.Checker) error From 9b8a7ace95604feb48f1d783a64a608f8af79d36 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Fri, 28 Jun 2024 11:27:43 -0400 Subject: [PATCH 23/45] validation remaining the same for RegistrationV1; adding in validation for RegistrationV2; using switch type to determine which registration type is meant to be validated --- options.go | 175 +++++++++++++++++++++++++++++++++++++++-------------- webhook.go | 21 ++++--- 2 files changed, 142 insertions(+), 54 deletions(-) diff --git a/options.go b/options.go index fcf0571..4141bea 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ package webhook import ( + "errors" "fmt" "regexp" "time" @@ -20,7 +21,7 @@ type errorOption struct { err error } -func (e errorOption) Validate(*Registration) error { +func (e errorOption) Validate(any) error { return error(e.err) } @@ -39,9 +40,20 @@ func AtLeastOneEvent() Option { type atLeastOneEventOption struct{} -func (atLeastOneEventOption) Validate(r *Registration) error { - if len(r.Events) == 0 { - return fmt.Errorf("%w: cannot have zero events", ErrInvalidInput) +func (atLeastOneEventOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + if len(r.Events) == 0 { + return fmt.Errorf("%w: cannot have zero events", ErrInvalidInput) + } + case *RegistrationV2: + { + if len(r.Matcher) == 0 { + return fmt.Errorf("%w: must have Matcher for events", ErrInvalidInput) + } + } + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } return nil @@ -58,13 +70,26 @@ func EventRegexMustCompile() Option { type eventRegexMustCompileOption struct{} -func (eventRegexMustCompileOption) Validate(r *Registration) error { - for _, e := range r.Events { - _, err := regexp.Compile(e) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) +func (eventRegexMustCompileOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + for _, e := range r.Events { + _, err := regexp.Compile(e) + if err != nil { + return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) + } } + case *RegistrationV2: + for _, m := range r.Matcher { + _, err := regexp.Compile(m.Regex) + if err != nil { + return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) + } + } + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } + return nil } @@ -80,13 +105,21 @@ func DeviceIDRegexMustCompile() Option { type deviceIDRegexMustCompileOption struct{} -func (deviceIDRegexMustCompileOption) Validate(r *Registration) error { - for _, e := range r.Matcher.DeviceID { - _, err := regexp.Compile(e) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) +func (deviceIDRegexMustCompileOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + for _, e := range r.Matcher.DeviceID { + _, err := regexp.Compile(e) + if err != nil { + return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) + } } + case *RegistrationV2: + //Matcher description is for Events. Are we not matching for DeviceId in Reg2? + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } + return nil } @@ -107,37 +140,47 @@ type validateRegistrationDurationOption struct { ttl time.Duration } -func (v validateRegistrationDurationOption) Validate(r *Registration) error { - if v.ttl <= 0 { - v.ttl = time.Duration(0) - } - - if v.ttl != 0 && v.ttl < time.Duration(r.Duration) { - return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) - } - - if r.Until.IsZero() && r.Duration == 0 { - return fmt.Errorf("%w: either Duration or Until must be set", ErrInvalidInput) - } +func (v validateRegistrationDurationOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + if v.ttl <= 0 { + v.ttl = time.Duration(0) + } - if !r.Until.IsZero() && r.Duration != 0 { - return fmt.Errorf("%w: only one of Duration or Until may be set", ErrInvalidInput) - } + if v.ttl != 0 && v.ttl < time.Duration(r.Duration) { + return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) + } - if !r.Until.IsZero() { - nowFunc := time.Now - if r.nowFunc != nil { - nowFunc = r.nowFunc + if r.Until.IsZero() && r.Duration == 0 { + return fmt.Errorf("%w: either Duration or Until must be set", ErrInvalidInput) } - now := nowFunc() - if v.ttl != 0 && r.Until.After(now.Add(v.ttl)) { - return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) + if !r.Until.IsZero() && r.Duration != 0 { + return fmt.Errorf("%w: only one of Duration or Until may be set", ErrInvalidInput) } - if r.Until.Before(now) { + if !r.Until.IsZero() { + nowFunc := time.Now + if r.nowFunc != nil { + nowFunc = r.nowFunc + } + + now := nowFunc() + if v.ttl != 0 && r.Until.After(now.Add(v.ttl)) { + return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) + } + + if r.Until.Before(now) { + return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) + } + } + case *RegistrationV2: + now := time.Now() + if now.After(r.Expires) { return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) } + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } return nil @@ -157,8 +200,16 @@ type provideTimeNowFuncOption struct { nowFunc func() time.Time } -func (p provideTimeNowFuncOption) Validate(r *Registration) error { - r.nowFunc = p.nowFunc +func (p provideTimeNowFuncOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + r.nowFunc = p.nowFunc + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not have nowFunc.", ErrInvalidOption) + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1", ErrInvalidType) + } + return nil } @@ -179,13 +230,23 @@ type provideFailureURLValidatorOption struct { checker *urlegit.Checker } -func (p provideFailureURLValidatorOption) Validate(r *Registration) error { +func (p provideFailureURLValidatorOption) Validate(i any) error { + var failureURL string if p.checker == nil { return nil } - if r.FailureURL != "" { - if err := p.checker.Text(r.FailureURL); err != nil { + switch r := i.(type) { + case *RegistrationV1: + failureURL = r.FailureURL + case *RegistrationV2: + failureURL = r.FailureURL + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + } + + if failureURL != "" { + if err := p.checker.Text(failureURL); err != nil { return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) } } @@ -209,16 +270,36 @@ type provideReceiverURLValidatorOption struct { checker *urlegit.Checker } -func (p provideReceiverURLValidatorOption) Validate(r *Registration) error { +func (p provideReceiverURLValidatorOption) Validate(i any) error { if p.checker == nil { return nil } - if r.Config.ReceiverURL != "" { - if err := p.checker.Text(r.Config.ReceiverURL); err != nil { - return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) + switch r := i.(type) { + case *RegistrationV1: + if r.Config.ReceiverURL != "" { + if err := p.checker.Text(r.Config.ReceiverURL); err != nil { + return fmt.Errorf("%w: receiver url is invalid", ErrInvalidInput) + } } + case *RegistrationV2: + var errs error + for _, w := range r.Webhooks { + for _, url := range w.ReceiverURLs { + if url != "" { + if err := p.checker.Text(url); err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: receiver url [%v] is invalid for webhook [%v]", ErrInvalidInput, url, w)) + } + } + } + } + if errs != nil { + return errs + } + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } + return nil } @@ -239,7 +320,7 @@ type provideAlternativeURLValidatorOption struct { checker *urlegit.Checker } -func (p provideAlternativeURLValidatorOption) Validate(r *Registration) error { +func (p provideAlternativeURLValidatorOption) Validate(i any) error { if p.checker == nil { return nil } diff --git a/webhook.go b/webhook.go index 00df611..fdb3db5 100644 --- a/webhook.go +++ b/webhook.go @@ -4,12 +4,15 @@ package webhook import ( + "errors" "fmt" "time" ) var ( - ErrInvalidInput = fmt.Errorf("invalid input") + ErrInvalidInput = fmt.Errorf("invalid input") + ErrInvalidType = fmt.Errorf("invalid type") + ErrInvalidOption = fmt.Errorf("invalid validation option") ) // Deprecated: This substructure should only be used for backwards compatibility @@ -61,6 +64,9 @@ type RegistrationV1 struct { // Until describes the time this subscription expires. Until time.Time `json:"until"` + + // now is a function that returns the current time. It is used for testing. + nowFunc func() time.Time `json:"-"` } type RetryHint struct { @@ -197,7 +203,7 @@ type RegistrationV2 struct { FailureURL string `json:"failure_url"` // Matcher is the list of regular expressions to match incoming events against to. - // Note. Any failures due to a bad regex feild or regex expression will result in a silent failure. + // Note. Any failures due to a bad regex field or regex expression will result in a silent failure. Matcher []FieldRegex `json:"matcher,omitempty"` // Expires describes the time this subscription expires. @@ -207,18 +213,19 @@ type RegistrationV2 struct { type Option interface { fmt.Stringer - Validate(*Registration) error + Validate(any) error } -// Validate is a method on Registration that validates the registration +// Validate is a method that validates the registration // against a list of options. -func (r *Registration) Validate(opts ...Option) error { +func Validate[R RegistrationV1 | RegistrationV2](r R, opts ...Option) error { + var errs error for _, opt := range opts { if opt != nil { if err := opt.Validate(r); err != nil { - return err + errs = errors.Join(errs, err) } } } - return nil + return errs } From b11e74f61ddc707c669fc917a3acc87be4927437 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Fri, 28 Jun 2024 11:45:50 -0400 Subject: [PATCH 24/45] added in more validation for RegistrationV2 and updated run_tests --- options.go | 33 ++++++++++++++++++----- options_test.go | 69 ++++++++++++++++++++++++++----------------------- 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/options.go b/options.go index 4141bea..b69f096 100644 --- a/options.go +++ b/options.go @@ -325,11 +325,23 @@ func (p provideAlternativeURLValidatorOption) Validate(i any) error { return nil } - for _, url := range r.Config.AlternativeURLs { - if err := p.checker.Text(url); err != nil { - return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) + switch r := i.(type) { + case *RegistrationV1: + var errs error + for _, url := range r.Config.AlternativeURLs { + if err := p.checker.Text(url); err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: alternative url [%v] is invalid", ErrInvalidInput, url)) + } } + if errs != nil { + return errs + } + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not use alternative urls. Use ProvideReceiverURLValidator() to validate non-failure urls", ErrInvalidOption) + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } + return nil } @@ -347,10 +359,19 @@ func NoUntil() Option { type noUntilOption struct{} -func (noUntilOption) Validate(r *Registration) error { - if !r.Until.IsZero() { - return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) +func (noUntilOption) Validate(i any) error { + + switch r := i.(type) { + case *RegistrationV1: + if !r.Until.IsZero() { + return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) + } + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not use an Until field", ErrInvalidOption) + default: + return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } + return nil } diff --git a/options_test.go b/options_test.go index e1e0696..36bff8a 100644 --- a/options_test.go +++ b/options_test.go @@ -14,7 +14,7 @@ import ( type optionTest struct { description string - in Registration + in any opt Option opts []Option str string @@ -44,12 +44,12 @@ func TestAtLeastOneEventOption(t *testing.T) { { description: "there is an event", opt: AtLeastOneEvent(), - in: Registration{Events: []string{"foo"}}, + in: RegistrationV1{Events: []string{"foo"}}, str: "AtLeastOneEvent()", }, { description: "multiple events", opt: AtLeastOneEvent(), - in: Registration{Events: []string{"foo", "bar"}}, + in: RegistrationV1{Events: []string{"foo", "bar"}}, str: "AtLeastOneEvent()", }, { description: "there are no events", @@ -64,17 +64,17 @@ func TestEventRegexMustCompile(t *testing.T) { { description: "the regex compiles", opt: EventRegexMustCompile(), - in: Registration{Events: []string{"event.*"}}, + in: RegistrationV1{Events: []string{"event.*"}}, str: "EventRegexMustCompile()", }, { description: "multiple events", opt: EventRegexMustCompile(), - in: Registration{Events: []string{"magic-thing", "event.*"}}, + in: RegistrationV1{Events: []string{"magic-thing", "event.*"}}, str: "EventRegexMustCompile()", }, { description: "failure", opt: EventRegexMustCompile(), - in: Registration{Events: []string{"("}}, + in: RegistrationV1{Events: []string{"("}}, expectedErr: ErrInvalidInput, }, }) @@ -85,7 +85,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { { description: "the regex compiles", opt: DeviceIDRegexMustCompile(), - in: Registration{ + in: RegistrationV1{ Matcher: MetadataMatcherConfig{ DeviceID: []string{"device.*"}, }, @@ -94,7 +94,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { }, { description: "multiple device ids", opt: DeviceIDRegexMustCompile(), - in: Registration{ + in: RegistrationV1{ Matcher: MetadataMatcherConfig{ DeviceID: []string{"device.*", "magic-thing"}, }, @@ -103,7 +103,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { }, { description: "failure", opt: DeviceIDRegexMustCompile(), - in: Registration{ + in: RegistrationV1{ Matcher: MetadataMatcherConfig{ DeviceID: []string{"("}, }, @@ -121,33 +121,33 @@ func TestValidateRegistrationDuration(t *testing.T) { { description: "success with time in bounds", opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ + in: RegistrationV1{ Duration: CustomDuration(4 * time.Minute), }, str: "ValidateRegistrationDuration(5m0s)", }, { description: "success with time in bounds, exactly", opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ + in: RegistrationV1{ Duration: CustomDuration(5 * time.Minute), }, }, { description: "failure with time out of bounds", opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ + in: RegistrationV1{ Duration: CustomDuration(6 * time.Minute), }, expectedErr: ErrInvalidInput, }, { description: "success with max ttl ignored", opt: ValidateRegistrationDuration(-5 * time.Minute), - in: Registration{ + in: RegistrationV1{ Duration: CustomDuration(1 * time.Minute), }, }, { description: "success with max ttl ignored, 0 duration", opt: ValidateRegistrationDuration(0), - in: Registration{ + in: RegistrationV1{ Duration: CustomDuration(1 * time.Minute), }, }, { @@ -156,7 +156,7 @@ func TestValidateRegistrationDuration(t *testing.T) { ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute), }, - in: Registration{ + in: RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), }, }, { @@ -165,7 +165,7 @@ func TestValidateRegistrationDuration(t *testing.T) { ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now), }, - in: Registration{ + in: RegistrationV1{ Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, @@ -175,7 +175,7 @@ func TestValidateRegistrationDuration(t *testing.T) { ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute), }, - in: Registration{ + in: RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), }, }, { @@ -184,7 +184,7 @@ func TestValidateRegistrationDuration(t *testing.T) { ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now), }, - in: Registration{ + in: RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, @@ -194,7 +194,7 @@ func TestValidateRegistrationDuration(t *testing.T) { ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute), }, - in: Registration{ + in: RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, @@ -204,13 +204,13 @@ func TestValidateRegistrationDuration(t *testing.T) { ProvideTimeNowFunc(now), ValidateRegistrationDuration(0), }, - in: Registration{ + in: RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), }, }, { description: "failure, both expirations set", opt: ValidateRegistrationDuration(5 * time.Minute), - in: Registration{ + in: RegistrationV1{ Duration: CustomDuration(1 * time.Minute), Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), }, @@ -254,14 +254,14 @@ func TestProvideFailureURLValidator(t *testing.T) { }, { description: "success, with checker", opt: ProvideFailureURLValidator(checker), - in: Registration{ + in: RegistrationV1{ FailureURL: "https://example.com", }, str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "failure, with checker", opt: ProvideFailureURLValidator(checker), - in: Registration{ + in: RegistrationV1{ FailureURL: "http://example.com", }, expectedErr: ErrInvalidInput, @@ -282,7 +282,7 @@ func TestProvideReceiverURLValidator(t *testing.T) { }, { description: "success, with checker", opt: ProvideReceiverURLValidator(checker), - in: Registration{ + in: RegistrationV1{ Config: DeliveryConfig{ ReceiverURL: "https://example.com", }, @@ -291,7 +291,7 @@ func TestProvideReceiverURLValidator(t *testing.T) { }, { description: "failure, with checker", opt: ProvideReceiverURLValidator(checker), - in: Registration{ + in: RegistrationV1{ Config: DeliveryConfig{ ReceiverURL: "http://example.com", }, @@ -314,7 +314,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "success, with checker", opt: ProvideAlternativeURLValidator(checker), - in: Registration{ + in: RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"https://example.com"}, }, @@ -323,7 +323,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "success, with checker and multiple urls", opt: ProvideAlternativeURLValidator(checker), - in: Registration{ + in: RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"https://example.com", "https://example.org"}, }, @@ -332,7 +332,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "failure, with checker", opt: ProvideAlternativeURLValidator(checker), - in: Registration{ + in: RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"http://example.com"}, }, @@ -341,7 +341,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "failure, with checker with multiple urls", opt: ProvideAlternativeURLValidator(checker), - in: Registration{ + in: RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"https://example.com", "http://example.com"}, }, @@ -360,7 +360,7 @@ func TestNoUntil(t *testing.T) { }, { description: "detect until set", opt: NoUntil(), - in: Registration{ + in: RegistrationV1{ Until: time.Now(), }, expectedErr: ErrInvalidInput, @@ -371,9 +371,14 @@ func run_tests(t *testing.T, tests []optionTest) { for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { assert := assert.New(t) - + var err error opts := append(tc.opts, tc.opt) - err := tc.in.Validate(opts...) + switch r := tc.in.(type) { + case RegistrationV1: + err = Validate[RegistrationV1](r, opts...) + case RegistrationV2: + err = Validate[RegistrationV2](r, opts...) + } assert.ErrorIs(err, tc.expectedErr) From 56c9dd900bccb387ff5be7671bbed225681b77e7 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 1 Jul 2024 15:58:31 -0400 Subject: [PATCH 25/45] added test cases for registrationV2 and default cases --- options.go | 13 +-- options_test.go | 274 +++++++++++++++++++++++++++++++++++++----------- webhook.go | 2 +- 3 files changed, 220 insertions(+), 69 deletions(-) diff --git a/options.go b/options.go index b69f096..48d1b52 100644 --- a/options.go +++ b/options.go @@ -48,12 +48,10 @@ func (atLeastOneEventOption) Validate(i any) error { } case *RegistrationV2: { - if len(r.Matcher) == 0 { - return fmt.Errorf("%w: must have Matcher for events", ErrInvalidInput) - } + return fmt.Errorf("%w: RegistrationV2 does not have an events field to validate", ErrInvalidType) } default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return fmt.Errorf("%w: Registration must be of type RegistrationV1", ErrInvalidType) } return nil @@ -204,10 +202,6 @@ func (p provideTimeNowFuncOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: r.nowFunc = p.nowFunc - case *RegistrationV2: - return fmt.Errorf("%w: RegistrationV2 does not have nowFunc.", ErrInvalidOption) - default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1", ErrInvalidType) } return nil @@ -232,6 +226,7 @@ type provideFailureURLValidatorOption struct { func (p provideFailureURLValidatorOption) Validate(i any) error { var failureURL string + //TODO: do we want to move this check to be inside each case statement? if p.checker == nil { return nil } @@ -337,7 +332,7 @@ func (p provideAlternativeURLValidatorOption) Validate(i any) error { return errs } case *RegistrationV2: - return fmt.Errorf("%w: RegistrationV2 does not use alternative urls. Use ProvideReceiverURLValidator() to validate non-failure urls", ErrInvalidOption) + return fmt.Errorf("%w: RegistrationV2 does not have an alternative urls field. Use ProvideReceiverURLValidator() to validate all non-failure urls", ErrInvalidOption) default: return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) } diff --git a/options_test.go b/options_test.go index 36bff8a..b0b1c03 100644 --- a/options_test.go +++ b/options_test.go @@ -25,13 +25,24 @@ func TestErrorOption(t *testing.T) { run_tests(t, []optionTest{ { description: "success", + in: &RegistrationV1{}, str: "foo", - }, { - description: "simple error", + }, + { + description: "simple error - RegistrationV1", opt: Error(ErrInvalidInput), str: "Error('invalid input')", expectedErr: ErrInvalidInput, - }, { + in: &RegistrationV1{}, + }, + { + description: "simple error - RegistrationV2", + opt: Error(ErrInvalidInput), + str: "Error('invalid input')", + expectedErr: ErrInvalidInput, + in: &RegistrationV2{}, + }, + { description: "simple nil error", opt: Error(nil), str: "Error(nil)", @@ -42,41 +53,94 @@ func TestErrorOption(t *testing.T) { func TestAtLeastOneEventOption(t *testing.T) { run_tests(t, []optionTest{ { - description: "there is an event", + description: "there is an event - V1", opt: AtLeastOneEvent(), - in: RegistrationV1{Events: []string{"foo"}}, + in: &RegistrationV1{Events: []string{"foo"}}, str: "AtLeastOneEvent()", }, { - description: "multiple events", + description: "multiple events - V1", opt: AtLeastOneEvent(), - in: RegistrationV1{Events: []string{"foo", "bar"}}, + in: &RegistrationV1{Events: []string{"foo", "bar"}}, str: "AtLeastOneEvent()", }, { - description: "there are no events", + description: "there are no events - V1", opt: AtLeastOneEvent(), + in: &RegistrationV1{}, expectedErr: ErrInvalidInput, }, + { + description: "invalid type - RegistrationV2", + opt: AtLeastOneEvent(), + in: &RegistrationV2{}, + expectedErr: ErrInvalidType, + }, + { + description: "default case - invalid", + opt: AtLeastOneEvent(), + expectedErr: ErrInvalidType, + }, }) } func TestEventRegexMustCompile(t *testing.T) { run_tests(t, []optionTest{ { - description: "the regex compiles", + description: "the regex compiles - V1", opt: EventRegexMustCompile(), - in: RegistrationV1{Events: []string{"event.*"}}, + in: &RegistrationV1{Events: []string{"event.*"}}, str: "EventRegexMustCompile()", }, { description: "multiple events", opt: EventRegexMustCompile(), - in: RegistrationV1{Events: []string{"magic-thing", "event.*"}}, + in: &RegistrationV1{Events: []string{"magic-thing", "event.*"}}, str: "EventRegexMustCompile()", }, { - description: "failure", + description: "failure - V1", + opt: EventRegexMustCompile(), + in: &RegistrationV1{Events: []string{"("}}, + expectedErr: ErrInvalidInput, + }, + { + description: "the regex compiles - V2", + opt: EventRegexMustCompile(), + in: &RegistrationV2{Matcher: []FieldRegex{ + { + Field: "canonical_name", + Regex: "webpa", + }, + }}, + str: "EventRegexMustCompile()", + }, + { + description: "multiple matchers - V2", + opt: EventRegexMustCompile(), + in: &RegistrationV2{Matcher: []FieldRegex{ + { + Field: "canonical_name", + Regex: "webpa", + }, + { + Field: "address", + Regex: "www.example.com", + }, + }}, + str: "EventRegexMustCompile()", + }, + { + description: "failure - V2", opt: EventRegexMustCompile(), - in: RegistrationV1{Events: []string{"("}}, + in: &RegistrationV2{Matcher: []FieldRegex{ + { + Regex: "(", + }, + }}, expectedErr: ErrInvalidInput, }, + { + description: "default case - invalid", + opt: EventRegexMustCompile(), + expectedErr: ErrInvalidType, + }, }) } @@ -85,7 +149,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { { description: "the regex compiles", opt: DeviceIDRegexMustCompile(), - in: RegistrationV1{ + in: &RegistrationV1{ Matcher: MetadataMatcherConfig{ DeviceID: []string{"device.*"}, }, @@ -94,7 +158,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { }, { description: "multiple device ids", opt: DeviceIDRegexMustCompile(), - in: RegistrationV1{ + in: &RegistrationV1{ Matcher: MetadataMatcherConfig{ DeviceID: []string{"device.*", "magic-thing"}, }, @@ -103,13 +167,18 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { }, { description: "failure", opt: DeviceIDRegexMustCompile(), - in: RegistrationV1{ + in: &RegistrationV1{ Matcher: MetadataMatcherConfig{ DeviceID: []string{"("}, }, }, expectedErr: ErrInvalidInput, }, + { + description: "default case - invalid", + opt: DeviceIDRegexMustCompile(), + expectedErr: ErrInvalidType, + }, }) } @@ -119,106 +188,119 @@ func TestValidateRegistrationDuration(t *testing.T) { } run_tests(t, []optionTest{ { - description: "success with time in bounds", + description: "success with time in bounds - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: RegistrationV1{ + in: &RegistrationV1{ Duration: CustomDuration(4 * time.Minute), }, str: "ValidateRegistrationDuration(5m0s)", }, { - description: "success with time in bounds, exactly", + description: "success with time in bounds, exactly - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: RegistrationV1{ + in: &RegistrationV1{ Duration: CustomDuration(5 * time.Minute), }, }, { - description: "failure with time out of bounds", + description: "failure with time out of bounds - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: RegistrationV1{ + in: &RegistrationV1{ Duration: CustomDuration(6 * time.Minute), }, expectedErr: ErrInvalidInput, }, { - description: "success with max ttl ignored", + description: "success with max ttl ignored - V1", opt: ValidateRegistrationDuration(-5 * time.Minute), - in: RegistrationV1{ + in: &RegistrationV1{ Duration: CustomDuration(1 * time.Minute), }, }, { - description: "success with max ttl ignored, 0 duration", + description: "success with max ttl ignored, 0 duration - V1", opt: ValidateRegistrationDuration(0), - in: RegistrationV1{ + in: &RegistrationV1{ Duration: CustomDuration(1 * time.Minute), }, }, { - description: "success with until in bounds", + description: "success with until in bounds - V1", opts: []Option{ ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute), }, - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), }, }, { - description: "failure due to until being before now", + description: "failure due to until being before now - V1", opts: []Option{ ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now), }, - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, }, { - description: "success with until exactly in bounds", + description: "success with until exactly in bounds - V1", opts: []Option{ ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute), }, - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), }, }, { - description: "failure due to the options being out of order", + description: "failure due to the options being out of order - V1", opts: []Option{ ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now), }, - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, }, { - description: "failure with until out of bounds", + description: "failure with until out of bounds - V1", opts: []Option{ ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute), }, - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, }, { - description: "success with until just needing to be present", + description: "success with until just needing to be present - V1", opts: []Option{ ProvideTimeNowFunc(now), ValidateRegistrationDuration(0), }, - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), }, }, { - description: "failure, both expirations set", + description: "failure, both expirations set - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: RegistrationV1{ + in: &RegistrationV1{ Duration: CustomDuration(1 * time.Minute), Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, }, { - description: "failure, no expiration set", + description: "failure, no expiration set - V1", + in: &RegistrationV1{}, opt: ValidateRegistrationDuration(5 * time.Minute), expectedErr: ErrInvalidInput, + }, { + description: "failure, exipred - V2", + in: &RegistrationV2{ + Expires: now(), + }, + opt: ValidateRegistrationDuration(0), + expectedErr: ErrInvalidInput, + }, + { + description: "default case - invalid", + opt: ValidateRegistrationDuration(5 * time.Minute), + expectedErr: ErrInvalidType, }, }) } @@ -252,19 +334,37 @@ func TestProvideFailureURLValidator(t *testing.T) { opt: ProvideFailureURLValidator(nil), str: "ProvideFailureURLValidator(nil)", }, { - description: "success, with checker", + description: "success, with checker - V1", opt: ProvideFailureURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ FailureURL: "https://example.com", }, str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { - description: "failure, with checker", + description: "failure, with checker - V1", + opt: ProvideFailureURLValidator(checker), + in: &RegistrationV1{ + FailureURL: "http://example.com", + }, + expectedErr: ErrInvalidInput, + }, { + description: "success, with checker - V2", + opt: ProvideFailureURLValidator(checker), + in: &RegistrationV2{ + FailureURL: "https://example.com", + }, + str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker - V2", opt: ProvideFailureURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV2{ FailureURL: "http://example.com", }, expectedErr: ErrInvalidInput, + }, { + description: "default case - invalid", + opt: ProvideFailureURLValidator(checker), + expectedErr: ErrInvalidType, }, }) } @@ -280,23 +380,51 @@ func TestProvideReceiverURLValidator(t *testing.T) { opt: ProvideReceiverURLValidator(nil), str: "ProvideReceiverURLValidator(nil)", }, { - description: "success, with checker", + description: "success, with checker - V1", opt: ProvideReceiverURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ Config: DeliveryConfig{ ReceiverURL: "https://example.com", }, }, str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { - description: "failure, with checker", + description: "failure, with checker - V1", opt: ProvideReceiverURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ Config: DeliveryConfig{ ReceiverURL: "http://example.com", }, }, expectedErr: ErrInvalidInput, + }, { + description: "success, with checker - V2", + opt: ProvideReceiverURLValidator(checker), + in: &RegistrationV2{ + Webhooks: []Webhook{ + { + ReceiverURLs: []string{"https://example.com", + "https://example2.com"}, + }, + }, + }, + str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker - V2", + opt: ProvideReceiverURLValidator(checker), + in: &RegistrationV2{ + Webhooks: []Webhook{ + { + ReceiverURLs: []string{"https://example.com", + "http://example2.com"}, + }, + }, + }, + expectedErr: ErrInvalidInput, + }, { + description: "default case - invalid", + opt: ProvideReceiverURLValidator(checker), + expectedErr: ErrInvalidType, }, }) } @@ -314,7 +442,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "success, with checker", opt: ProvideAlternativeURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"https://example.com"}, }, @@ -323,7 +451,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "success, with checker and multiple urls", opt: ProvideAlternativeURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"https://example.com", "https://example.org"}, }, @@ -332,7 +460,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "failure, with checker", opt: ProvideAlternativeURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"http://example.com"}, }, @@ -341,12 +469,21 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "failure, with checker with multiple urls", opt: ProvideAlternativeURLValidator(checker), - in: RegistrationV1{ + in: &RegistrationV1{ Config: DeliveryConfig{ AlternativeURLs: []string{"https://example.com", "http://example.com"}, }, }, expectedErr: ErrInvalidInput, + }, { + description: "failure - RegistrationV2", + opt: ProvideAlternativeURLValidator(checker), + in: &RegistrationV2{}, + expectedErr: ErrInvalidOption, + }, { + description: "default case - invalid", + opt: ProvideAlternativeURLValidator(checker), + expectedErr: ErrInvalidType, }, }) } @@ -355,18 +492,33 @@ func TestNoUntil(t *testing.T) { run_tests(t, []optionTest{ { description: "success, no until set", + in: &RegistrationV1{}, opt: NoUntil(), str: "NoUntil()", }, { description: "detect until set", opt: NoUntil(), - in: RegistrationV1{ + in: &RegistrationV1{ Until: time.Now(), }, expectedErr: ErrInvalidInput, }, + { + description: "failure - V2", + opt: NoUntil(), + in: &RegistrationV2{}, + expectedErr: ErrInvalidOption, + }, + { + description: "default case - invalid", + opt: NoUntil(), + expectedErr: ErrInvalidType, + }, }) } + +type EmptyStruct struct{} + func run_tests(t *testing.T, tests []optionTest) { for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { @@ -374,12 +526,16 @@ func run_tests(t *testing.T, tests []optionTest) { var err error opts := append(tc.opts, tc.opt) switch r := tc.in.(type) { - case RegistrationV1: - err = Validate[RegistrationV1](r, opts...) - case RegistrationV2: - err = Validate[RegistrationV2](r, opts...) + case *RegistrationV1: + err = Validate(r, opts...) + case *RegistrationV2: + err = Validate(r, opts...) + default: + for _, o := range opts { + err = o.Validate(nil) + assert.ErrorIs(err, tc.expectedErr) + } } - assert.ErrorIs(err, tc.expectedErr) if tc.str != "" && tc.opt != nil { diff --git a/webhook.go b/webhook.go index fdb3db5..5b6a975 100644 --- a/webhook.go +++ b/webhook.go @@ -218,7 +218,7 @@ type Option interface { // Validate is a method that validates the registration // against a list of options. -func Validate[R RegistrationV1 | RegistrationV2](r R, opts ...Option) error { +func Validate[R *RegistrationV1 | *RegistrationV2](r R, opts ...Option) error { var errs error for _, opt := range opts { if opt != nil { From 9564e84d81f8135d9834e0d439a51d96f40692ad Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 1 Jul 2024 16:03:53 -0400 Subject: [PATCH 26/45] Revert "Update webhook.go" This reverts commit d0621ca5d54509639155e5ab1a59ca212852974b. --- webhook.go | 1 - 1 file changed, 1 deletion(-) diff --git a/webhook.go b/webhook.go index b1f9c9c..52e0ccf 100644 --- a/webhook.go +++ b/webhook.go @@ -74,7 +74,6 @@ type RegistrationV1 struct { // now is a function that returns the current time. It is used for testing. nowFunc func() time.Time `json:"-"` } - type RetryHint struct { //RetryEachUrl is the amount of times a URL should be retried given a failed response until the next URL in the request is tried. //Default value will be set to none From dda637fdcefd8fd7196e34fa39db576a8605afe9 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 1 Jul 2024 16:08:01 -0400 Subject: [PATCH 27/45] Revert "Revert "Update webhook.go"" This reverts commit 9564e84d81f8135d9834e0d439a51d96f40692ad. --- webhook.go | 1 + 1 file changed, 1 insertion(+) diff --git a/webhook.go b/webhook.go index 52e0ccf..b1f9c9c 100644 --- a/webhook.go +++ b/webhook.go @@ -74,6 +74,7 @@ type RegistrationV1 struct { // now is a function that returns the current time. It is used for testing. nowFunc func() time.Time `json:"-"` } + type RetryHint struct { //RetryEachUrl is the amount of times a URL should be retried given a failed response until the next URL in the request is tried. //Default value will be set to none From 2e1fc047c968df54981cbceef84e57b62a102158 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 1 Jul 2024 16:19:31 -0400 Subject: [PATCH 28/45] Revert "updatedthe until option validation" This reverts commit 8cd830206517a65c4ac013074c851d49e82a9f3f. --- options.go | 18 +++++------------- validation.go | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/options.go b/options.go index 9a6423b..e522ac1 100644 --- a/options.go +++ b/options.go @@ -248,22 +248,14 @@ func (noUntilOption) String() string { return "NoUntil()" } -func Until(j time.Duration, m time.Duration, now func() time.Time) Option { - return untilOption{ - jitter: j, - max: m, - now: now, - } +func Until() Option { + return untilOption{} } -type untilOption struct { - jitter time.Duration - max time.Duration - now func() time.Time -} +type untilOption struct{} -func (u untilOption) Validate(val Validator) error { - if err := val.ValidateUntil(u.jitter, u.max, u.now); err != nil { +func (untilOption) Validate(val Validator) error { + if err := val.ValidateUntil(); err != nil { return err } return nil diff --git a/validation.go b/validation.go index 48db5fc..775a336 100644 --- a/validation.go +++ b/validation.go @@ -10,7 +10,7 @@ type Validator interface { ValidateOneEvent() error ValidateEventRegex() error ValidateDeviceId() error - ValidateUntil(time.Duration, time.Duration, func() time.Time) error + ValidateUntil() error ValidateNoUntil() error ValidateDuration(time.Duration) error ValidateFailureURL(*urlegit.Checker) error From 491ef4b3b98b968232420d6ada0b0c7e9481872e Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 1 Jul 2024 16:20:41 -0400 Subject: [PATCH 29/45] Revert "added back in nowFunc to registrationv1 and added validation for it" This reverts commit d30c20fb0fe4166a5ee94fd4fa2693599425a95d. --- options.go | 38 +++++++++++++++++++------------------- validation.go | 1 - webhook.go | 13 +++---------- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/options.go b/options.go index e522ac1..2bdb1d8 100644 --- a/options.go +++ b/options.go @@ -126,25 +126,25 @@ func (v validateRegistrationDurationOption) String() string { // ProvideTimeNowFunc is an option that allows the caller to provide a function // that returns the current time. This is used for testing. -func ProvideTimeNowFunc(nowFunc func() time.Time) Option { - return provideTimeNowFuncOption{nowFunc: nowFunc} -} - -type provideTimeNowFuncOption struct { - nowFunc func() time.Time -} - -func (p provideTimeNowFuncOption) Validate(val Validator) error { - val.SetNowFunc(p.nowFunc) - return nil -} - -func (p provideTimeNowFuncOption) String() string { - if p.nowFunc == nil { - return "ProvideTimeNowFunc(nil)" - } - return "ProvideTimeNowFunc(func)" -} +// func ProvideTimeNowFunc(nowFunc func() time.Time) Option { +// return provideTimeNowFuncOption{nowFunc: nowFunc} +// } + +// type provideTimeNowFuncOption struct { +// nowFunc func() time.Time +// } + +// func (p provideTimeNowFuncOption) Validate(val Validator) error { +// r.nowFunc = p.nowFunc +// return nil +// } + +// func (p provideTimeNowFuncOption) String() string { +// if p.nowFunc == nil { +// return "ProvideTimeNowFunc(nil)" +// } +// return "ProvideTimeNowFunc(func)" +// } // ProvideFailureURLValidator is an option that allows the caller to provide a // URL validator that is used to validate the FailureURL. diff --git a/validation.go b/validation.go index 775a336..84f937a 100644 --- a/validation.go +++ b/validation.go @@ -16,7 +16,6 @@ type Validator interface { ValidateFailureURL(*urlegit.Checker) error ValidateReceiverURL(*urlegit.Checker) error ValidateAltURL(*urlegit.Checker) error - SetNowFunc(func() time.Time) } type ValidatorConfig struct { diff --git a/webhook.go b/webhook.go index b1f9c9c..bb7a9ea 100644 --- a/webhook.go +++ b/webhook.go @@ -70,9 +70,6 @@ type RegistrationV1 struct { // Until describes the time this subscription expires. Until time.Time `json:"until"` - - // now is a function that returns the current time. It is used for testing. - nowFunc func() time.Time `json:"-"` } type RetryHint struct { @@ -285,9 +282,9 @@ func (v1 *RegistrationV1) ValidateDuration(ttl time.Duration) error { if !v1.Until.IsZero() { nowFunc := time.Now - if v1.nowFunc != nil { - nowFunc = v1.nowFunc - } + // if v1.nowFunc != nil { + // nowFunc = v1.nowFunc + // } now := nowFunc() if ttl != 0 && v1.Until.After(now.Add(ttl)) { @@ -359,7 +356,3 @@ func (v1 *RegistrationV1) ValidateUntil(jitter time.Duration, maxTTL time.Durati return nil } - -func (v1 *RegistrationV1) SetNowFunc(now func() time.Time) { - v1.nowFunc = now -} From 5a7e41cf9d10797a6b88f9ee3fad4ee8a2194ae1 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 1 Jul 2024 16:44:01 -0400 Subject: [PATCH 30/45] added tests for AlwaysValid option --- options_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/options_test.go b/options_test.go index 972a103..1067522 100644 --- a/options_test.go +++ b/options_test.go @@ -50,6 +50,16 @@ func TestErrorOption(t *testing.T) { }) } +func TestAlwaysOption(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "success", + opt: AlwaysValid(), + in: &RegistrationV1{}, + str: "alwaysValidOption", + }, + }) +} func TestAtLeastOneEventOption(t *testing.T) { run_tests(t, []optionTest{ { From 90baaab039972c5dbc9af4a10d5061d46429cd31 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 2 Jul 2024 14:23:58 -0400 Subject: [PATCH 31/45] removing the register interface from webhook-schema and moving it to ancla --- webhook.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/webhook.go b/webhook.go index ba94f8c..5b6a975 100644 --- a/webhook.go +++ b/webhook.go @@ -15,11 +15,6 @@ var ( ErrInvalidOption = fmt.Errorf("invalid validation option") ) -type Register interface { - GetId() string - GetUntil() time.Time -} - // Deprecated: This substructure should only be used for backwards compatibility // matching. Use Webhook instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. From e317ca8c788a9d82b190ecf66b7f3c185432b1e3 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 3 Jul 2024 10:03:50 -0400 Subject: [PATCH 32/45] added back in the url validation logic that was originally in ancla --- validationConfig.go | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 validationConfig.go diff --git a/validationConfig.go b/validationConfig.go new file mode 100644 index 0000000..fa6b2bb --- /dev/null +++ b/validationConfig.go @@ -0,0 +1,74 @@ +package webhook + +import ( + "time" + + "github.com/xmidt-org/urlegit" +) + +var ( + SpecialUseIPs = []string{ + "0.0.0.0/8", //local ipv4 + "fe80::/10", //local ipv6 + "255.255.255.255/32", //broadcast to neighbors + "2001::/32", //ipv6 TEREDO prefix + "2001:5::/32", //EID space for lisp + "2002::/16", //ipv6 6to4 + "fc00::/7", //ipv6 unique local + "192.0.0.0/24", //ipv4 IANA + "2001:0000::/23", //ipv6 IANA + "224.0.0.1/32", //ipv4 multicast + } + SpecialUseHosts = []string{ + ".example.", + ".invalid.", + ".test.", + "localhost", + } +) + +type ValidatorConfig struct { + URL URLVConfig + TTL TTLVConfig +} + +type URLVConfig struct { + HTTPSOnly bool + AllowLoopback bool + AllowIP bool + AllowSpecialUseHosts bool + AllowSpecialUseIPs bool + InvalidHosts []string + InvalidSubnets []string +} + +type TTLVConfig struct { + Max time.Duration + Jitter time.Duration + Now func() time.Time +} + +// BuildURLChecker translates the configuration into url Checker to be run on the webhook. +func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { + var o []urlegit.Option + if config.URL.HTTPSOnly { + o = append(o, urlegit.OnlyAllowSchemes("https")) + } + if !config.URL.AllowLoopback { + o = append(o, urlegit.ForbidLoopback()) + } + if !config.URL.AllowIP { + o = append(o, urlegit.ForbidAnyIPs()) + } + if !config.URL.AllowSpecialUseHosts { + o = append(o, urlegit.ForbidSpecialUseDomains()) + } + if !config.URL.AllowSpecialUseIPs { + o = append(o, urlegit.ForbidSubnets(SpecialUseIPs)) + } + checker, err := urlegit.New(o...) + if err != nil { + return nil, err + } + return checker, nil +} From 316cfea3c7769bb1c79947e97ca8c29e825767e2 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 3 Jul 2024 10:04:57 -0400 Subject: [PATCH 33/45] this is an option for how we can set up the Validation Options --- validationConfig.go | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/validationConfig.go b/validationConfig.go index fa6b2bb..646f162 100644 --- a/validationConfig.go +++ b/validationConfig.go @@ -28,8 +28,9 @@ var ( ) type ValidatorConfig struct { - URL URLVConfig - TTL TTLVConfig + URL URLVConfig + TTL TTLVConfig + Options OptionsConfig } type URLVConfig struct { @@ -48,7 +49,17 @@ type TTLVConfig struct { Now func() time.Time } -// BuildURLChecker translates the configuration into url Checker to be run on the webhook. +type OptionsConfig struct { + AtLeastOneEvent bool + EventRegexMustCompile bool + DeviceIDRegexMustCompile bool + ValidateRegistrationDuration bool + ProvideReceiverURLValidator bool + ProvideFailureURLValidator bool + ProvideAlternativeURLValidator bool +} + +// BuildURLChecker translates the configuration into url Checker to be run on the registration. func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { var o []urlegit.Option if config.URL.HTTPSOnly { @@ -72,3 +83,27 @@ func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { } return checker, nil } + +//BuildOptions translates the configuration into a list of options to be used to validate the registration +func BuildOptions(config ValidatorConfig, checker *urlegit.Checker) []Option { + var opts []Option + if config.Options.AtLeastOneEvent { + opts = append(opts, AtLeastOneEvent()) + } + if config.Options.EventRegexMustCompile { + opts = append(opts, EventRegexMustCompile()) + } + if config.Options.DeviceIDRegexMustCompile { + opts = append(opts, DeviceIDRegexMustCompile()) + } + if config.Options.ProvideReceiverURLValidator { + opts = append(opts, ProvideReceiverURLValidator(checker)) + } + if config.Options.ProvideFailureURLValidator { + opts = append(opts, ProvideFailureURLValidator(checker)) + } + if config.Options.ProvideAlternativeURLValidator { + opts = append(opts, ProvideAlternativeURLValidator(checker)) + } + return opts +} From d343c39862a0b493608cb9922e188de896fb5aba Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 8 Jul 2024 12:40:59 -0400 Subject: [PATCH 34/45] added back in the struct methods to clean up code; removed the generic validate function to improve testing --- options.go | 137 +++++++-------------------------------- options_test.go | 31 ++++----- webhook.go | 166 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 198 insertions(+), 136 deletions(-) diff --git a/options.go b/options.go index 453e229..691bd6e 100644 --- a/options.go +++ b/options.go @@ -4,9 +4,7 @@ package webhook import ( - "errors" "fmt" - "regexp" "time" "github.com/xmidt-org/urlegit" @@ -57,18 +55,12 @@ type atLeastOneEventOption struct{} func (atLeastOneEventOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - if len(r.Events) == 0 { - return fmt.Errorf("%w: cannot have zero events", ErrInvalidInput) - } + return r.ValidateOneEvent() case *RegistrationV2: - { - return fmt.Errorf("%w: RegistrationV2 does not have an events field to validate", ErrInvalidType) - } + return fmt.Errorf("%w: RegistrationV2 does not have an events field to validate", ErrInvalidType) default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1", ErrInvalidType) + return ErrUknownType } - - return nil } func (atLeastOneEventOption) String() string { @@ -85,24 +77,12 @@ type eventRegexMustCompileOption struct{} func (eventRegexMustCompileOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - for _, e := range r.Events { - _, err := regexp.Compile(e) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) - } - } + return r.ValidateEventRegex() case *RegistrationV2: - for _, m := range r.Matcher { - _, err := regexp.Compile(m.Regex) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) - } - } + return r.ValidateEventRegex() default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } - - return nil } func (eventRegexMustCompileOption) String() string { @@ -120,16 +100,11 @@ type deviceIDRegexMustCompileOption struct{} func (deviceIDRegexMustCompileOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - for _, e := range r.Matcher.DeviceID { - _, err := regexp.Compile(e) - if err != nil { - return fmt.Errorf("%w: unable to compile matching", ErrInvalidInput) - } - } + return r.ValidateDeviceId() case *RegistrationV2: //Matcher description is for Events. Are we not matching for DeviceId in Reg2? default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } return nil @@ -155,47 +130,12 @@ type validateRegistrationDurationOption struct { func (v validateRegistrationDurationOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - if v.ttl <= 0 { - v.ttl = time.Duration(0) - } - - if v.ttl != 0 && v.ttl < time.Duration(r.Duration) { - return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) - } - - if r.Until.IsZero() && r.Duration == 0 { - return fmt.Errorf("%w: either Duration or Until must be set", ErrInvalidInput) - } - - if !r.Until.IsZero() && r.Duration != 0 { - return fmt.Errorf("%w: only one of Duration or Until may be set", ErrInvalidInput) - } - - if !r.Until.IsZero() { - nowFunc := time.Now - if r.nowFunc != nil { - nowFunc = r.nowFunc - } - - now := nowFunc() - if v.ttl != 0 && r.Until.After(now.Add(v.ttl)) { - return fmt.Errorf("%w: the registration is for too long", ErrInvalidInput) - } - - if r.Until.Before(now) { - return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) - } - } + return r.ValidateDuration(v.ttl) case *RegistrationV2: - now := time.Now() - if now.After(r.Expires) { - return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) - } + return r.ValidateDuration() default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } - - return nil } func (v validateRegistrationDurationOption) String() string { @@ -215,7 +155,7 @@ type provideTimeNowFuncOption struct { func (p provideTimeNowFuncOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - r.nowFunc = p.nowFunc + r.SetNowFunc(p.nowFunc) } return nil @@ -251,7 +191,7 @@ func (p provideFailureURLValidatorOption) Validate(i any) error { case *RegistrationV2: failureURL = r.FailureURL default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } if failureURL != "" { @@ -286,30 +226,12 @@ func (p provideReceiverURLValidatorOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - if r.Config.ReceiverURL != "" { - if err := p.checker.Text(r.Config.ReceiverURL); err != nil { - return fmt.Errorf("%w: receiver url is invalid", ErrInvalidInput) - } - } + return r.ValidateReceiverURL(p.checker) case *RegistrationV2: - var errs error - for _, w := range r.Webhooks { - for _, url := range w.ReceiverURLs { - if url != "" { - if err := p.checker.Text(url); err != nil { - errs = errors.Join(errs, fmt.Errorf("%w: receiver url [%v] is invalid for webhook [%v]", ErrInvalidInput, url, w)) - } - } - } - } - if errs != nil { - return errs - } + return r.ValidateReceiverURL(p.checker) default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } - - return nil } func (p provideReceiverURLValidatorOption) String() string { @@ -336,22 +258,12 @@ func (p provideAlternativeURLValidatorOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - var errs error - for _, url := range r.Config.AlternativeURLs { - if err := p.checker.Text(url); err != nil { - errs = errors.Join(errs, fmt.Errorf("%w: alternative url [%v] is invalid", ErrInvalidInput, url)) - } - } - if errs != nil { - return errs - } + return r.ValidateAltURL(p.checker) case *RegistrationV2: - return fmt.Errorf("%w: RegistrationV2 does not have an alternative urls field. Use ProvideReceiverURLValidator() to validate all non-failure urls", ErrInvalidOption) + return fmt.Errorf("%w: RegistrationV2 does not have an alternative urls field. Use ProvideReceiverURLValidator() to validate all non-failure urls", ErrInvalidType) default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } - - return nil } func (p provideAlternativeURLValidatorOption) String() string { @@ -372,18 +284,15 @@ func (noUntilOption) Validate(i any) error { switch r := i.(type) { case *RegistrationV1: - if !r.Until.IsZero() { - return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) - } + return r.ValidateNoUntil() case *RegistrationV2: - return fmt.Errorf("%w: RegistrationV2 does not use an Until field", ErrInvalidOption) + return fmt.Errorf("%w: RegistrationV2 does not use an Until field", ErrInvalidType) default: - return fmt.Errorf("%w: Registration must be of type RegistrationV1 or RegistrationV2", ErrInvalidType) + return ErrUknownType } - return nil } func (noUntilOption) String() string { return "NoUntil()" -} \ No newline at end of file +} diff --git a/options_test.go b/options_test.go index 1067522..7ae9528 100644 --- a/options_test.go +++ b/options_test.go @@ -16,7 +16,7 @@ type optionTest struct { description string in any opt Option - opts []Option + opts Validators str string expectedErr error } @@ -87,7 +87,7 @@ func TestAtLeastOneEventOption(t *testing.T) { { description: "default case - invalid", opt: AtLeastOneEvent(), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -149,7 +149,7 @@ func TestEventRegexMustCompile(t *testing.T) { { description: "default case - invalid", opt: EventRegexMustCompile(), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -187,7 +187,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { { description: "default case - invalid", opt: DeviceIDRegexMustCompile(), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -310,7 +310,7 @@ func TestValidateRegistrationDuration(t *testing.T) { { description: "default case - invalid", opt: ValidateRegistrationDuration(5 * time.Minute), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -374,7 +374,7 @@ func TestProvideFailureURLValidator(t *testing.T) { }, { description: "default case - invalid", opt: ProvideFailureURLValidator(checker), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -434,7 +434,7 @@ func TestProvideReceiverURLValidator(t *testing.T) { }, { description: "default case - invalid", opt: ProvideReceiverURLValidator(checker), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -489,11 +489,11 @@ func TestProvideAlternativeURLValidator(t *testing.T) { description: "failure - RegistrationV2", opt: ProvideAlternativeURLValidator(checker), in: &RegistrationV2{}, - expectedErr: ErrInvalidOption, + expectedErr: ErrInvalidType, }, { description: "default case - invalid", opt: ProvideAlternativeURLValidator(checker), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -517,12 +517,12 @@ func TestNoUntil(t *testing.T) { description: "failure - V2", opt: NoUntil(), in: &RegistrationV2{}, - expectedErr: ErrInvalidOption, + expectedErr: ErrInvalidType, }, { description: "default case - invalid", opt: NoUntil(), - expectedErr: ErrInvalidType, + expectedErr: ErrUknownType, }, }) } @@ -535,14 +535,11 @@ func run_tests(t *testing.T, tests []optionTest) { opts := append(tc.opts, tc.opt) switch r := tc.in.(type) { case *RegistrationV1: - err = Validate(r, opts...) + err = opts.Validate(r) case *RegistrationV2: - err = Validate(r, opts...) + err = opts.Validate(r) default: - for _, o := range opts { - err = o.Validate(nil) - assert.ErrorIs(err, tc.expectedErr) - } + err = opts.Validate(nil) } assert.ErrorIs(err, tc.expectedErr) diff --git a/webhook.go b/webhook.go index 5b6a975..c0a3a4a 100644 --- a/webhook.go +++ b/webhook.go @@ -6,13 +6,16 @@ package webhook import ( "errors" "fmt" + "regexp" "time" + + "github.com/xmidt-org/urlegit" ) var ( - ErrInvalidInput = fmt.Errorf("invalid input") - ErrInvalidType = fmt.Errorf("invalid type") - ErrInvalidOption = fmt.Errorf("invalid validation option") + ErrInvalidInput = fmt.Errorf("invalid input") + ErrInvalidType = fmt.Errorf("invalid type") + ErrUknownType = fmt.Errorf("unknown type") ) // Deprecated: This substructure should only be used for backwards compatibility @@ -215,12 +218,13 @@ type Option interface { fmt.Stringer Validate(any) error } +type Validators []Option // Validate is a method that validates the registration // against a list of options. -func Validate[R *RegistrationV1 | *RegistrationV2](r R, opts ...Option) error { +func (vs Validators) Validate(r any) error { var errs error - for _, opt := range opts { + for _, opt := range vs { if opt != nil { if err := opt.Validate(r); err != nil { errs = errors.Join(errs, err) @@ -229,3 +233,155 @@ func Validate[R *RegistrationV1 | *RegistrationV2](r R, opts ...Option) error { } return errs } + +func (v1 *RegistrationV1) ValidateOneEvent() error { + if len(v1.Events) == 0 { + return fmt.Errorf("%w: cannot have zero events", ErrInvalidInput) + } + return nil +} + +func (v1 *RegistrationV1) ValidateEventRegex() error { + var errs error + for _, e := range v1.Events { + _, err := regexp.Compile(e) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: unable to compile matching", ErrInvalidInput)) + } + } + return errs +} + +func (v1 *RegistrationV1) ValidateDeviceId() error { + var errs error + for _, e := range v1.Matcher.DeviceID { + _, err := regexp.Compile(e) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: unable to compile matching", ErrInvalidInput)) + } + } + return errs +} + +func (v1 *RegistrationV1) ValidateDuration(ttl time.Duration) error { + var errs error + if ttl <= 0 { + ttl = time.Duration(0) + } + + if ttl != 0 && ttl < time.Duration(v1.Duration) { + errs = errors.Join(errs, fmt.Errorf("%w: the registration is for too long", ErrInvalidInput)) + } + + if v1.Until.IsZero() && v1.Duration == 0 { + errs = errors.Join(errs, fmt.Errorf("%w: either Duration or Until must be set", ErrInvalidInput)) + } + + if !v1.Until.IsZero() && v1.Duration != 0 { + errs = errors.Join(errs, fmt.Errorf("%w: only one of Duration or Until may be set", ErrInvalidInput)) + } + + if !v1.Until.IsZero() { + nowFunc := time.Now + if v1.nowFunc != nil { + nowFunc = v1.nowFunc + } + + now := nowFunc() + if ttl != 0 && v1.Until.After(now.Add(ttl)) { + errs = errors.Join(errs, fmt.Errorf("%w: the registration is for too long", ErrInvalidInput)) + } + + if v1.Until.Before(now) { + errs = errors.Join(errs, fmt.Errorf("%w: the registration has already expired", ErrInvalidInput)) + } + } + + return errs +} + +func (v1 *RegistrationV1) ValidateReceiverURL(c *urlegit.Checker) error { + if v1.Config.ReceiverURL != "" { + if err := c.Text(v1.Config.ReceiverURL); err != nil { + return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) + } + } + return nil +} + +func (v1 *RegistrationV1) ValidateAltURL(c *urlegit.Checker) error { + var errs error + for _, url := range v1.Config.AlternativeURLs { + if err := c.Text(url); err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: failure url is invalid", ErrInvalidInput)) + } + } + return errs +} + +func (v1 *RegistrationV1) ValidateNoUntil() error { + if !v1.Until.IsZero() { + return fmt.Errorf("%w: Until is not allowed", ErrInvalidInput) + } + return nil +} + +func (v1 *RegistrationV1) ValidateUntil(jitter time.Duration, maxTTL time.Duration, now func() time.Time) error { + if now == nil { + now = time.Now + } + if maxTTL < 0 { + return ErrInvalidInput + } else if jitter < 0 { + return ErrInvalidInput + } + + if v1.Until.IsZero() { + return nil + } + limit := (now().Add(maxTTL)).Add(jitter) + proposed := (v1.Until) + if proposed.After(limit) { + return fmt.Errorf("%w: %v after %v", + ErrInvalidInput, proposed.String(), limit.String()) + } + return nil + +} + +func (v1 *RegistrationV1) SetNowFunc(now func() time.Time) { + v1.nowFunc = now +} + +func (v2 *RegistrationV2) ValidateEventRegex() error { + var errs error + for _, m := range v2.Matcher { + _, err := regexp.Compile(m.Regex) + if err != nil { + errs = errors.Join(fmt.Errorf("%w: %v", ErrInvalidInput, err)) + } + } + return errs +} + +func (v2 *RegistrationV2) ValidateDuration() error { + now := time.Now() + if now.After(v2.Expires) { + return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) + } + return nil +} + +func (v2 *RegistrationV2) ValidateReceiverURL(checker *urlegit.Checker) error { + var errs error + for _, w := range v2.Webhooks { + for _, url := range w.ReceiverURLs { + if url != "" { + if err := checker.Text(url); err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: receiver url [%v] is invalid for webhook [%v]", ErrInvalidInput, url, w)) + } + } + } + } + return errs +} From 84187f4c5869eb14f574e8066f6c7b302dcaf298 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 8 Jul 2024 12:44:32 -0400 Subject: [PATCH 35/45] added back in BuildValidators as I am going to be editing in a future PR --- validationConfig.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/validationConfig.go b/validationConfig.go index fa6b2bb..c5e5fd9 100644 --- a/validationConfig.go +++ b/validationConfig.go @@ -72,3 +72,24 @@ func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { } return checker, nil } + +// BuildValidators translates the configuration into a list of validators to be run on the +// webhook. +func BuildValidators(config ValidatorConfig) ([]Option, error) { + var opts []Option + + checker, err := BuildURLChecker(config) + if err != nil { + return nil, err + } + opts = append(opts, + AtLeastOneEvent(), + EventRegexMustCompile(), + DeviceIDRegexMustCompile(), + ValidateRegistrationDuration(config.TTL.Max), + ProvideReceiverURLValidator(checker), + ProvideFailureURLValidator(checker), + ProvideAlternativeURLValidator(checker), + ) + return opts, nil +} From c1be8274026e7aa9042c1e7481af4586eb9d68f6 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 8 Jul 2024 12:50:28 -0400 Subject: [PATCH 36/45] moved specialuseips back to original spot --- validationConfig.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/validationConfig.go b/validationConfig.go index c5e5fd9..6825801 100644 --- a/validationConfig.go +++ b/validationConfig.go @@ -6,27 +6,6 @@ import ( "github.com/xmidt-org/urlegit" ) -var ( - SpecialUseIPs = []string{ - "0.0.0.0/8", //local ipv4 - "fe80::/10", //local ipv6 - "255.255.255.255/32", //broadcast to neighbors - "2001::/32", //ipv6 TEREDO prefix - "2001:5::/32", //EID space for lisp - "2002::/16", //ipv6 6to4 - "fc00::/7", //ipv6 unique local - "192.0.0.0/24", //ipv4 IANA - "2001:0000::/23", //ipv6 IANA - "224.0.0.1/32", //ipv4 multicast - } - SpecialUseHosts = []string{ - ".example.", - ".invalid.", - ".test.", - "localhost", - } -) - type ValidatorConfig struct { URL URLVConfig TTL TTLVConfig @@ -48,6 +27,27 @@ type TTLVConfig struct { Now func() time.Time } +var ( + SpecialUseIPs = []string{ + "0.0.0.0/8", //local ipv4 + "fe80::/10", //local ipv6 + "255.255.255.255/32", //broadcast to neighbors + "2001::/32", //ipv6 TEREDO prefix + "2001:5::/32", //EID space for lisp + "2002::/16", //ipv6 6to4 + "fc00::/7", //ipv6 unique local + "192.0.0.0/24", //ipv4 IANA + "2001:0000::/23", //ipv6 IANA + "224.0.0.1/32", //ipv4 multicast + } + SpecialUseHosts = []string{ + ".example.", + ".invalid.", + ".test.", + "localhost", + } +) + // BuildURLChecker translates the configuration into url Checker to be run on the webhook. func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { var o []urlegit.Option From 2eb87c9a023ca212dff548f490607a415f34f5b7 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 8 Jul 2024 12:53:11 -0400 Subject: [PATCH 37/45] fixed spacing --- options.go | 1 - 1 file changed, 1 deletion(-) diff --git a/options.go b/options.go index 691bd6e..2299c73 100644 --- a/options.go +++ b/options.go @@ -281,7 +281,6 @@ func NoUntil() Option { type noUntilOption struct{} func (noUntilOption) Validate(i any) error { - switch r := i.(type) { case *RegistrationV1: return r.ValidateNoUntil() From 6ecbde7f04fde2abf82d1d869fc2f00e9e72029f Mon Sep 17 00:00:00 2001 From: maura fortino Date: Mon, 8 Jul 2024 13:15:49 -0400 Subject: [PATCH 38/45] fixed merge conflicts i missed --- validationConfig.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/validationConfig.go b/validationConfig.go index 8dbb497..58df635 100644 --- a/validationConfig.go +++ b/validationConfig.go @@ -85,7 +85,6 @@ func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { return checker, nil } -<<<<<<< HEAD //BuildOptions translates the configuration into a list of options to be used to validate the registration func BuildOptions(config ValidatorConfig, checker *urlegit.Checker) []Option { var opts []Option @@ -108,25 +107,4 @@ func BuildOptions(config ValidatorConfig, checker *urlegit.Checker) []Option { opts = append(opts, ProvideAlternativeURLValidator(checker)) } return opts -======= -// BuildValidators translates the configuration into a list of validators to be run on the -// webhook. -func BuildValidators(config ValidatorConfig) ([]Option, error) { - var opts []Option - - checker, err := BuildURLChecker(config) - if err != nil { - return nil, err - } - opts = append(opts, - AtLeastOneEvent(), - EventRegexMustCompile(), - DeviceIDRegexMustCompile(), - ValidateRegistrationDuration(config.TTL.Max), - ProvideReceiverURLValidator(checker), - ProvideFailureURLValidator(checker), - ProvideAlternativeURLValidator(checker), - ) - return opts, nil ->>>>>>> validation-v3 } From 638f3d419f91a58b5ced76ccb50b74f54f23e600 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 9 Jul 2024 09:49:13 -0400 Subject: [PATCH 39/45] fixed spacing --- options.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/options.go b/options.go index 2299c73..7bf23d9 100644 --- a/options.go +++ b/options.go @@ -106,7 +106,6 @@ func (deviceIDRegexMustCompileOption) Validate(i any) error { default: return ErrUknownType } - return nil } @@ -289,7 +288,6 @@ func (noUntilOption) Validate(i any) error { default: return ErrUknownType } - } func (noUntilOption) String() string { From 9947a7da30616b3fafc859e1ab5a65d300f1f882 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 9 Jul 2024 15:57:59 -0400 Subject: [PATCH 40/45] updated tests --- validationConfig.go | 11 +- validationConfig_test.go | 221 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 validationConfig_test.go diff --git a/validationConfig.go b/validationConfig.go index 58df635..114e17e 100644 --- a/validationConfig.go +++ b/validationConfig.go @@ -61,7 +61,7 @@ var ( ) // BuildURLChecker translates the configuration into url Checker to be run on the webhook. -func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { +func (config *ValidatorConfig) GetValidator() (*urlegit.Checker, error) { var o []urlegit.Option if config.URL.HTTPSOnly { o = append(o, urlegit.OnlyAllowSchemes("https")) @@ -78,14 +78,13 @@ func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { if !config.URL.AllowSpecialUseIPs { o = append(o, urlegit.ForbidSubnets(SpecialUseIPs)) } - checker, err := urlegit.New(o...) - if err != nil { - return nil, err + if len(config.URL.InvalidSubnets) > 0 { + o = append(o, urlegit.ForbidSubnets(config.URL.InvalidSubnets)) } - return checker, nil + return urlegit.New(o...) } -//BuildOptions translates the configuration into a list of options to be used to validate the registration +// BuildOptions translates the configuration into a list of options to be used to validate the registration func BuildOptions(config ValidatorConfig, checker *urlegit.Checker) []Option { var opts []Option if config.Options.AtLeastOneEvent { diff --git a/validationConfig_test.go b/validationConfig_test.go new file mode 100644 index 0000000..0707aea --- /dev/null +++ b/validationConfig_test.go @@ -0,0 +1,221 @@ +package webhook + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + mockNow = func() time.Time { + return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + } + mockMax = 5 * time.Minute + mockJitter = 5 * time.Second + buildAllConfig = ValidatorConfig{ + URL: URLVConfig{ + HTTPSOnly: true, + AllowLoopback: false, + AllowIP: false, + AllowSpecialUseHosts: false, + AllowSpecialUseIPs: false, + InvalidHosts: []string{}, + InvalidSubnets: []string{}, + }, + TTL: TTLVConfig{ + Max: mockMax, + Jitter: mockJitter, + Now: mockNow, + }, + } + buildNoneConfig = ValidatorConfig{ + URL: URLVConfig{ + HTTPSOnly: false, + AllowLoopback: true, + AllowIP: true, + AllowSpecialUseHosts: true, + AllowSpecialUseIPs: true, + InvalidHosts: []string{}, + InvalidSubnets: []string{}, + }, + TTL: TTLVConfig{ + Max: mockMax, + Jitter: mockJitter, + Now: mockNow, + }, + } +) + +func TestBuildValidURLFuncs(t *testing.T) { + tcs := []struct { + desc string + config ValidatorConfig + expectedErr error + expectedFuncCount int + }{ + // { + // desc: "HTTPSOnly only", + // config: ValidatorConfig{ + // URL: URLVConfig{ + // HTTPSOnly: true, + // AllowLoopback: true, + // AllowIP: true, + // AllowSpecialUseHosts: true, + // AllowSpecialUseIPs: true, + // }, + // }, + // expectedFuncCount: 1, + // }, + // { + // desc: "AllowLoopback only", + // config: ValidatorConfig{ + // URL: URLVConfig{ + // HTTPSOnly: false, + // AllowLoopback: false, + // AllowIP: true, + // AllowSpecialUseHosts: true, + // AllowSpecialUseIPs: true, + // }, + // }, + // expectedFuncCount: 2, + // }, + // { + // desc: "AllowIp Only", + // config: ValidatorConfig{ + // URL: URLVConfig{ + // HTTPSOnly: false, + // AllowLoopback: true, + // AllowIP: false, + // AllowSpecialUseHosts: true, + // AllowSpecialUseIPs: true, + // }, + // }, + // expectedFuncCount: 2, + // }, + // { + // desc: "AllowSpecialUseHosts Only", + // config: ValidatorConfig{ + // URL: URLVConfig{ + // HTTPSOnly: false, + // AllowLoopback: true, + // AllowIP: true, + // AllowSpecialUseHosts: false, + // AllowSpecialUseIPs: true, + // }, + // }, + // expectedFuncCount: 2, + // }, + // { + // desc: "AllowSpecialuseIPS Only", + // config: ValidatorConfig{ + // URL: URLVConfig{ + // HTTPSOnly: false, + // AllowLoopback: true, + // AllowIP: true, + // AllowSpecialUseHosts: true, + // AllowSpecialUseIPs: false, + // }, + // }, + // expectedFuncCount: 2, + // }, + { + desc: "InvalidSubnet Failure", + config: ValidatorConfig{ + URL: URLVConfig{ + HTTPSOnly: false, + AllowLoopback: true, + AllowIP: true, + AllowSpecialUseHosts: true, + AllowSpecialUseIPs: false, + InvalidSubnets: []string{"https://localhost:9000"}, + }, + }, + expectedErr: fmt.Errorf("error"), + }, + // { + // desc: "Build None", + // config: ValidatorConfig{ + // URL: buildNoneConfig.URL, + // }, + // expectedFuncCount: 1, + // }, + // { + // desc: "Build All", + // config: ValidatorConfig{ + // URL: buildAllConfig.URL, + // }, + // expectedFuncCount: 5, + // }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + assert := assert.New(t) + vals, err := tc.config.GetValidator() + if tc.expectedErr != nil { + assert.True(errors.Is(err, tc.expectedErr), + fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", + err, tc.expectedErr)) + assert.Nil(vals) + return + } + require.NoError(t, err) + }) + } +} + +func TestBuildValidators(t *testing.T) { + tcs := []struct { + desc string + config ValidatorConfig + expectedErr error + expectedFuncCount int + }{ + { + desc: "BuildValidURLFuncs Failure", + config: ValidatorConfig{ + URL: URLVConfig{ + HTTPSOnly: false, + AllowLoopback: true, + AllowIP: true, + AllowSpecialUseHosts: true, + AllowSpecialUseIPs: false, + InvalidSubnets: []string{"https://localhost:9000"}, + }, + }, + expectedErr: fmt.Errorf("error"), + }, + { + desc: "CheckDuration Failure", + config: ValidatorConfig{ + TTL: TTLVConfig{ + Max: -1 * time.Second, + }, + }, + expectedErr: fmt.Errorf("error"), + }, + { + desc: "CheckUntil Failure", + config: ValidatorConfig{ + TTL: TTLVConfig{ + Jitter: -1 * time.Second, + }, + }, + expectedErr: fmt.Errorf("error"), + }, + { + desc: "All Validators Added", + expectedFuncCount: 8, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + assert := assert.New(t) + opts := BuildOptions(tc.config, nil) + assert.NotNil(opts) + }) + } +} From 9e6e62ddf3ef1f87664611650f9651c25493690f Mon Sep 17 00:00:00 2001 From: maura fortino Date: Tue, 9 Jul 2024 16:01:53 -0400 Subject: [PATCH 41/45] returning error for v2 deviceidregexmustcompile and updated unit test --- options.go | 3 +-- options_test.go | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/options.go b/options.go index 7bf23d9..edcc4bb 100644 --- a/options.go +++ b/options.go @@ -102,11 +102,10 @@ func (deviceIDRegexMustCompileOption) Validate(i any) error { case *RegistrationV1: return r.ValidateDeviceId() case *RegistrationV2: - //Matcher description is for Events. Are we not matching for DeviceId in Reg2? + return fmt.Errorf("%w: RegistrationV2 does not use DeviceID directly, use `FieldRegex` instead", ErrInvalidType) default: return ErrUknownType } - return nil } func (deviceIDRegexMustCompileOption) String() string { diff --git a/options_test.go b/options_test.go index 7ae9528..d9b066c 100644 --- a/options_test.go +++ b/options_test.go @@ -85,7 +85,7 @@ func TestAtLeastOneEventOption(t *testing.T) { expectedErr: ErrInvalidType, }, { - description: "default case - invalid", + description: "default case - unknown", opt: AtLeastOneEvent(), expectedErr: ErrUknownType, }, @@ -147,7 +147,7 @@ func TestEventRegexMustCompile(t *testing.T) { expectedErr: ErrInvalidInput, }, { - description: "default case - invalid", + description: "default case - unknown", opt: EventRegexMustCompile(), expectedErr: ErrUknownType, }, @@ -157,7 +157,7 @@ func TestEventRegexMustCompile(t *testing.T) { func TestDeviceIDRegexMustCompile(t *testing.T) { run_tests(t, []optionTest{ { - description: "the regex compiles", + description: "the regex compiles - v1", opt: DeviceIDRegexMustCompile(), in: &RegistrationV1{ Matcher: MetadataMatcherConfig{ @@ -166,7 +166,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { }, str: "DeviceIDRegexMustCompile()", }, { - description: "multiple device ids", + description: "multiple device ids - v1", opt: DeviceIDRegexMustCompile(), in: &RegistrationV1{ Matcher: MetadataMatcherConfig{ @@ -175,7 +175,7 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { }, str: "DeviceIDRegexMustCompile()", }, { - description: "failure", + description: "failure - v1", opt: DeviceIDRegexMustCompile(), in: &RegistrationV1{ Matcher: MetadataMatcherConfig{ @@ -185,7 +185,13 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { expectedErr: ErrInvalidInput, }, { - description: "default case - invalid", + description: "invalid type - v2", + opt: DeviceIDRegexMustCompile(), + in: &RegistrationV2{}, + expectedErr: ErrInvalidType, + }, + { + description: "default case - unknown", opt: DeviceIDRegexMustCompile(), expectedErr: ErrUknownType, }, @@ -308,7 +314,7 @@ func TestValidateRegistrationDuration(t *testing.T) { expectedErr: ErrInvalidInput, }, { - description: "default case - invalid", + description: "default case - unknown", opt: ValidateRegistrationDuration(5 * time.Minute), expectedErr: ErrUknownType, }, @@ -372,7 +378,7 @@ func TestProvideFailureURLValidator(t *testing.T) { }, expectedErr: ErrInvalidInput, }, { - description: "default case - invalid", + description: "default case - unknown", opt: ProvideFailureURLValidator(checker), expectedErr: ErrUknownType, }, @@ -432,7 +438,7 @@ func TestProvideReceiverURLValidator(t *testing.T) { }, expectedErr: ErrInvalidInput, }, { - description: "default case - invalid", + description: "default case - unknown", opt: ProvideReceiverURLValidator(checker), expectedErr: ErrUknownType, }, @@ -491,7 +497,7 @@ func TestProvideAlternativeURLValidator(t *testing.T) { in: &RegistrationV2{}, expectedErr: ErrInvalidType, }, { - description: "default case - invalid", + description: "default case - unknown", opt: ProvideAlternativeURLValidator(checker), expectedErr: ErrUknownType, }, @@ -520,7 +526,7 @@ func TestNoUntil(t *testing.T) { expectedErr: ErrInvalidType, }, { - description: "default case - invalid", + description: "default case - unknown", opt: NoUntil(), expectedErr: ErrUknownType, }, From 556f13986f2291a68d276696a4de2df2387e4403 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 10 Jul 2024 16:30:09 -0400 Subject: [PATCH 42/45] removed validation config as that is being added back into ancla --- validationConfig.go | 95 --------------------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 validationConfig.go diff --git a/validationConfig.go b/validationConfig.go deleted file mode 100644 index 6825801..0000000 --- a/validationConfig.go +++ /dev/null @@ -1,95 +0,0 @@ -package webhook - -import ( - "time" - - "github.com/xmidt-org/urlegit" -) - -type ValidatorConfig struct { - URL URLVConfig - TTL TTLVConfig -} - -type URLVConfig struct { - HTTPSOnly bool - AllowLoopback bool - AllowIP bool - AllowSpecialUseHosts bool - AllowSpecialUseIPs bool - InvalidHosts []string - InvalidSubnets []string -} - -type TTLVConfig struct { - Max time.Duration - Jitter time.Duration - Now func() time.Time -} - -var ( - SpecialUseIPs = []string{ - "0.0.0.0/8", //local ipv4 - "fe80::/10", //local ipv6 - "255.255.255.255/32", //broadcast to neighbors - "2001::/32", //ipv6 TEREDO prefix - "2001:5::/32", //EID space for lisp - "2002::/16", //ipv6 6to4 - "fc00::/7", //ipv6 unique local - "192.0.0.0/24", //ipv4 IANA - "2001:0000::/23", //ipv6 IANA - "224.0.0.1/32", //ipv4 multicast - } - SpecialUseHosts = []string{ - ".example.", - ".invalid.", - ".test.", - "localhost", - } -) - -// BuildURLChecker translates the configuration into url Checker to be run on the webhook. -func BuildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { - var o []urlegit.Option - if config.URL.HTTPSOnly { - o = append(o, urlegit.OnlyAllowSchemes("https")) - } - if !config.URL.AllowLoopback { - o = append(o, urlegit.ForbidLoopback()) - } - if !config.URL.AllowIP { - o = append(o, urlegit.ForbidAnyIPs()) - } - if !config.URL.AllowSpecialUseHosts { - o = append(o, urlegit.ForbidSpecialUseDomains()) - } - if !config.URL.AllowSpecialUseIPs { - o = append(o, urlegit.ForbidSubnets(SpecialUseIPs)) - } - checker, err := urlegit.New(o...) - if err != nil { - return nil, err - } - return checker, nil -} - -// BuildValidators translates the configuration into a list of validators to be run on the -// webhook. -func BuildValidators(config ValidatorConfig) ([]Option, error) { - var opts []Option - - checker, err := BuildURLChecker(config) - if err != nil { - return nil, err - } - opts = append(opts, - AtLeastOneEvent(), - EventRegexMustCompile(), - DeviceIDRegexMustCompile(), - ValidateRegistrationDuration(config.TTL.Max), - ProvideReceiverURLValidator(checker), - ProvideFailureURLValidator(checker), - ProvideAlternativeURLValidator(checker), - ) - return opts, nil -} From 8d55e4c25773805551a15eb8ef41bdc258d64861 Mon Sep 17 00:00:00 2001 From: maura fortino Date: Thu, 11 Jul 2024 09:44:08 -0400 Subject: [PATCH 43/45] fixed formatting issues --- options_test.go | 217 +++++++++++------------------------------------- 1 file changed, 49 insertions(+), 168 deletions(-) diff --git a/options_test.go b/options_test.go index d9b066c..5fd182f 100644 --- a/options_test.go +++ b/options_test.go @@ -113,37 +113,19 @@ func TestEventRegexMustCompile(t *testing.T) { { description: "the regex compiles - V2", opt: EventRegexMustCompile(), - in: &RegistrationV2{Matcher: []FieldRegex{ - { - Field: "canonical_name", - Regex: "webpa", - }, - }}, - str: "EventRegexMustCompile()", + in: &RegistrationV2{Matcher: []FieldRegex{{Field: "canonical_name", Regex: "webpa"}}}, + str: "EventRegexMustCompile()", }, { description: "multiple matchers - V2", opt: EventRegexMustCompile(), - in: &RegistrationV2{Matcher: []FieldRegex{ - { - Field: "canonical_name", - Regex: "webpa", - }, - { - Field: "address", - Regex: "www.example.com", - }, - }}, - str: "EventRegexMustCompile()", + in: &RegistrationV2{Matcher: []FieldRegex{{Field: "canonical_name", Regex: "webpa"}, {Field: "address", Regex: "www.example.com"}}}, + str: "EventRegexMustCompile()", }, { description: "failure - V2", opt: EventRegexMustCompile(), - in: &RegistrationV2{Matcher: []FieldRegex{ - { - Regex: "(", - }, - }}, + in: &RegistrationV2{Matcher: []FieldRegex{{Regex: "("}}}, expectedErr: ErrInvalidInput, }, { @@ -159,35 +141,23 @@ func TestDeviceIDRegexMustCompile(t *testing.T) { { description: "the regex compiles - v1", opt: DeviceIDRegexMustCompile(), - in: &RegistrationV1{ - Matcher: MetadataMatcherConfig{ - DeviceID: []string{"device.*"}, - }, - }, - str: "DeviceIDRegexMustCompile()", + in: &RegistrationV1{Matcher: MetadataMatcherConfig{DeviceID: []string{"device.*"}}}, + str: "DeviceIDRegexMustCompile()", }, { description: "multiple device ids - v1", opt: DeviceIDRegexMustCompile(), - in: &RegistrationV1{ - Matcher: MetadataMatcherConfig{ - DeviceID: []string{"device.*", "magic-thing"}, - }, - }, - str: "DeviceIDRegexMustCompile()", + in: &RegistrationV1{Matcher: MetadataMatcherConfig{DeviceID: []string{"device.*", "magic-thing"}}}, + str: "DeviceIDRegexMustCompile()", }, { description: "failure - v1", opt: DeviceIDRegexMustCompile(), - in: &RegistrationV1{ - Matcher: MetadataMatcherConfig{ - DeviceID: []string{"("}, - }, - }, + in: &RegistrationV1{Matcher: MetadataMatcherConfig{DeviceID: []string{"("}}}, expectedErr: ErrInvalidInput, }, { description: "invalid type - v2", - opt: DeviceIDRegexMustCompile(), - in: &RegistrationV2{}, + opt: DeviceIDRegexMustCompile(), + in: &RegistrationV2{}, expectedErr: ErrInvalidType, }, { @@ -206,99 +176,58 @@ func TestValidateRegistrationDuration(t *testing.T) { { description: "success with time in bounds - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: &RegistrationV1{ - Duration: CustomDuration(4 * time.Minute), - }, - str: "ValidateRegistrationDuration(5m0s)", + in: &RegistrationV1{Duration: CustomDuration(4 * time.Minute)}, + str: "ValidateRegistrationDuration(5m0s)", }, { description: "success with time in bounds, exactly - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: &RegistrationV1{ - Duration: CustomDuration(5 * time.Minute), - }, + in: &RegistrationV1{Duration: CustomDuration(5 * time.Minute)}, }, { description: "failure with time out of bounds - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: &RegistrationV1{ - Duration: CustomDuration(6 * time.Minute), - }, + in: &RegistrationV1{Duration: CustomDuration(6 * time.Minute)}, expectedErr: ErrInvalidInput, }, { description: "success with max ttl ignored - V1", opt: ValidateRegistrationDuration(-5 * time.Minute), - in: &RegistrationV1{ - Duration: CustomDuration(1 * time.Minute), - }, + in: &RegistrationV1{Duration: CustomDuration(1 * time.Minute)}, }, { description: "success with max ttl ignored, 0 duration - V1", opt: ValidateRegistrationDuration(0), - in: &RegistrationV1{ - Duration: CustomDuration(1 * time.Minute), - }, + in: &RegistrationV1{Duration: CustomDuration(1 * time.Minute)}, }, { description: "success with until in bounds - V1", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(5 * time.Minute), - }, - in: &RegistrationV1{ - Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), - }, + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC)}, }, { description: "failure due to until being before now - V1", - opts: []Option{ - ValidateRegistrationDuration(5 * time.Minute), - ProvideTimeNowFunc(now), - }, + opts: []Option{ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now)}, in: &RegistrationV1{ Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), }, expectedErr: ErrInvalidInput, }, { description: "success with until exactly in bounds - V1", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(5 * time.Minute), - }, - in: &RegistrationV1{ - Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), - }, + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC)}, }, { description: "failure due to the options being out of order - V1", - opts: []Option{ - ValidateRegistrationDuration(5 * time.Minute), - ProvideTimeNowFunc(now), - }, - in: &RegistrationV1{ - Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), - }, + opts: []Option{ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC)}, expectedErr: ErrInvalidInput, }, { description: "failure with until out of bounds - V1", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(5 * time.Minute), - }, - in: &RegistrationV1{ - Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), - }, + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC)}, expectedErr: ErrInvalidInput, }, { description: "success with until just needing to be present - V1", - opts: []Option{ - ProvideTimeNowFunc(now), - ValidateRegistrationDuration(0), - }, - in: &RegistrationV1{ - Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), - }, + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(0)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC)}, }, { description: "failure, both expirations set - V1", opt: ValidateRegistrationDuration(5 * time.Minute), - in: &RegistrationV1{ - Duration: CustomDuration(1 * time.Minute), - Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), - }, + in: &RegistrationV1{Duration: CustomDuration(1 * time.Minute), Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC)}, expectedErr: ErrInvalidInput, }, { description: "failure, no expiration set - V1", @@ -307,9 +236,7 @@ func TestValidateRegistrationDuration(t *testing.T) { expectedErr: ErrInvalidInput, }, { description: "failure, exipred - V2", - in: &RegistrationV2{ - Expires: now(), - }, + in: &RegistrationV2{Expires: now()}, opt: ValidateRegistrationDuration(0), expectedErr: ErrInvalidInput, }, @@ -352,30 +279,22 @@ func TestProvideFailureURLValidator(t *testing.T) { }, { description: "success, with checker - V1", opt: ProvideFailureURLValidator(checker), - in: &RegistrationV1{ - FailureURL: "https://example.com", - }, - str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + in: &RegistrationV1{FailureURL: "https://example.com"}, + str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "failure, with checker - V1", opt: ProvideFailureURLValidator(checker), - in: &RegistrationV1{ - FailureURL: "http://example.com", - }, + in: &RegistrationV1{FailureURL: "http://example.com"}, expectedErr: ErrInvalidInput, }, { description: "success, with checker - V2", opt: ProvideFailureURLValidator(checker), - in: &RegistrationV2{ - FailureURL: "https://example.com", - }, - str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + in: &RegistrationV2{FailureURL: "https://example.com"}, + str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "failure, with checker - V2", opt: ProvideFailureURLValidator(checker), - in: &RegistrationV2{ - FailureURL: "http://example.com", - }, + in: &RegistrationV2{FailureURL: "http://example.com"}, expectedErr: ErrInvalidInput, }, { description: "default case - unknown", @@ -398,44 +317,22 @@ func TestProvideReceiverURLValidator(t *testing.T) { }, { description: "success, with checker - V1", opt: ProvideReceiverURLValidator(checker), - in: &RegistrationV1{ - Config: DeliveryConfig{ - ReceiverURL: "https://example.com", - }, - }, - str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + in: &RegistrationV1{Config: DeliveryConfig{ReceiverURL: "https://example.com"}}, + str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "failure, with checker - V1", opt: ProvideReceiverURLValidator(checker), - in: &RegistrationV1{ - Config: DeliveryConfig{ - ReceiverURL: "http://example.com", - }, - }, + in: &RegistrationV1{Config: DeliveryConfig{ReceiverURL: "http://example.com"}}, expectedErr: ErrInvalidInput, }, { description: "success, with checker - V2", opt: ProvideReceiverURLValidator(checker), - in: &RegistrationV2{ - Webhooks: []Webhook{ - { - ReceiverURLs: []string{"https://example.com", - "https://example2.com"}, - }, - }, - }, - str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + in: &RegistrationV2{Webhooks: []Webhook{{ReceiverURLs: []string{"https://example.com", "https://example2.com"}}}}, + str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "failure, with checker - V2", opt: ProvideReceiverURLValidator(checker), - in: &RegistrationV2{ - Webhooks: []Webhook{ - { - ReceiverURLs: []string{"https://example.com", - "http://example2.com"}, - }, - }, - }, + in: &RegistrationV2{Webhooks: []Webhook{{ReceiverURLs: []string{"https://example.com", "http://example2.com"}}}}, expectedErr: ErrInvalidInput, }, { description: "default case - unknown", @@ -458,38 +355,22 @@ func TestProvideAlternativeURLValidator(t *testing.T) { }, { description: "success, with checker", opt: ProvideAlternativeURLValidator(checker), - in: &RegistrationV1{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"https://example.com"}, - }, - }, - str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"https://example.com"}}}, + str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "success, with checker and multiple urls", opt: ProvideAlternativeURLValidator(checker), - in: &RegistrationV1{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"https://example.com", "https://example.org"}, - }, - }, - str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"https://example.com", "https://example.org"}}}, + str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", }, { description: "failure, with checker", opt: ProvideAlternativeURLValidator(checker), - in: &RegistrationV1{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"http://example.com"}, - }, - }, + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"http://example.com"}}}, expectedErr: ErrInvalidInput, }, { description: "failure, with checker with multiple urls", opt: ProvideAlternativeURLValidator(checker), - in: &RegistrationV1{ - Config: DeliveryConfig{ - AlternativeURLs: []string{"https://example.com", "http://example.com"}, - }, - }, + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"https://example.com", "http://example.com"}}}, expectedErr: ErrInvalidInput, }, { description: "failure - RegistrationV2", From ffe3451a4e786c639291147025b78830633ee08d Mon Sep 17 00:00:00 2001 From: maura fortino Date: Wed, 17 Jul 2024 13:10:31 -0400 Subject: [PATCH 44/45] added the CheckUntil function from ancla --- options.go | 29 +++++ options_test.go | 16 +++ validationConfig.go | 109 ------------------- validationConfig_test.go | 221 --------------------------------------- webhook.go | 33 +++++- 5 files changed, 75 insertions(+), 333 deletions(-) delete mode 100644 validationConfig.go delete mode 100644 validationConfig_test.go diff --git a/options.go b/options.go index 7bf23d9..e0532d8 100644 --- a/options.go +++ b/options.go @@ -293,3 +293,32 @@ func (noUntilOption) Validate(i any) error { func (noUntilOption) String() string { return "NoUntil()" } + +func Until(now func() time.Time, jitter, max time.Duration) Option { + return untilOption{ + now: now, + jitter: jitter, + max: max, + } +} + +type untilOption struct { + now func() time.Time + jitter time.Duration + max time.Duration +} + +func (u untilOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + return r.CheckUntil(u.now, u.jitter, u.max) + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not use an Until field", ErrInvalidType) + default: + return ErrUknownType + } +} + +func (u untilOption) String() string { + return fmt.Sprintf("untilOption(%v, %v, %v)", u.jitter.String(), u.max.String(), u.now().String()) +} diff --git a/options_test.go b/options_test.go index 7ae9528..bce8910 100644 --- a/options_test.go +++ b/options_test.go @@ -12,6 +12,10 @@ import ( "github.com/xmidt-org/urlegit" ) +var mockNow = func() time.Time { + return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) +} + type optionTest struct { description string in any @@ -527,6 +531,18 @@ func TestNoUntil(t *testing.T) { }) } +// func TestUntilOption(t *testing.T) { +// run_tests(t, []optionTest{ +// { +// description: "success, until", +// in: &RegistrationV1{ +// Until: time.Now(), +// }, +// opt: Until(mockNow, time.Duration(1*time.Minute), time.Duration(5*time.Minute)), +// }, +// }) +// } + func run_tests(t *testing.T, tests []optionTest) { for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { diff --git a/validationConfig.go b/validationConfig.go deleted file mode 100644 index 114e17e..0000000 --- a/validationConfig.go +++ /dev/null @@ -1,109 +0,0 @@ -package webhook - -import ( - "time" - - "github.com/xmidt-org/urlegit" -) - -type ValidatorConfig struct { - URL URLVConfig - TTL TTLVConfig - Options OptionsConfig -} - -type URLVConfig struct { - HTTPSOnly bool - AllowLoopback bool - AllowIP bool - AllowSpecialUseHosts bool - AllowSpecialUseIPs bool - InvalidHosts []string - InvalidSubnets []string -} - -type TTLVConfig struct { - Max time.Duration - Jitter time.Duration - Now func() time.Time -} - -type OptionsConfig struct { - AtLeastOneEvent bool - EventRegexMustCompile bool - DeviceIDRegexMustCompile bool - ValidateRegistrationDuration bool - ProvideReceiverURLValidator bool - ProvideFailureURLValidator bool - ProvideAlternativeURLValidator bool -} - -// BuildURLChecker translates the configuration into url Checker to be run on the registration. -var ( - SpecialUseIPs = []string{ - "0.0.0.0/8", //local ipv4 - "fe80::/10", //local ipv6 - "255.255.255.255/32", //broadcast to neighbors - "2001::/32", //ipv6 TEREDO prefix - "2001:5::/32", //EID space for lisp - "2002::/16", //ipv6 6to4 - "fc00::/7", //ipv6 unique local - "192.0.0.0/24", //ipv4 IANA - "2001:0000::/23", //ipv6 IANA - "224.0.0.1/32", //ipv4 multicast - } - SpecialUseHosts = []string{ - ".example.", - ".invalid.", - ".test.", - "localhost", - } -) - -// BuildURLChecker translates the configuration into url Checker to be run on the webhook. -func (config *ValidatorConfig) GetValidator() (*urlegit.Checker, error) { - var o []urlegit.Option - if config.URL.HTTPSOnly { - o = append(o, urlegit.OnlyAllowSchemes("https")) - } - if !config.URL.AllowLoopback { - o = append(o, urlegit.ForbidLoopback()) - } - if !config.URL.AllowIP { - o = append(o, urlegit.ForbidAnyIPs()) - } - if !config.URL.AllowSpecialUseHosts { - o = append(o, urlegit.ForbidSpecialUseDomains()) - } - if !config.URL.AllowSpecialUseIPs { - o = append(o, urlegit.ForbidSubnets(SpecialUseIPs)) - } - if len(config.URL.InvalidSubnets) > 0 { - o = append(o, urlegit.ForbidSubnets(config.URL.InvalidSubnets)) - } - return urlegit.New(o...) -} - -// BuildOptions translates the configuration into a list of options to be used to validate the registration -func BuildOptions(config ValidatorConfig, checker *urlegit.Checker) []Option { - var opts []Option - if config.Options.AtLeastOneEvent { - opts = append(opts, AtLeastOneEvent()) - } - if config.Options.EventRegexMustCompile { - opts = append(opts, EventRegexMustCompile()) - } - if config.Options.DeviceIDRegexMustCompile { - opts = append(opts, DeviceIDRegexMustCompile()) - } - if config.Options.ProvideReceiverURLValidator { - opts = append(opts, ProvideReceiverURLValidator(checker)) - } - if config.Options.ProvideFailureURLValidator { - opts = append(opts, ProvideFailureURLValidator(checker)) - } - if config.Options.ProvideAlternativeURLValidator { - opts = append(opts, ProvideAlternativeURLValidator(checker)) - } - return opts -} diff --git a/validationConfig_test.go b/validationConfig_test.go deleted file mode 100644 index 0707aea..0000000 --- a/validationConfig_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package webhook - -import ( - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - mockNow = func() time.Time { - return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - } - mockMax = 5 * time.Minute - mockJitter = 5 * time.Second - buildAllConfig = ValidatorConfig{ - URL: URLVConfig{ - HTTPSOnly: true, - AllowLoopback: false, - AllowIP: false, - AllowSpecialUseHosts: false, - AllowSpecialUseIPs: false, - InvalidHosts: []string{}, - InvalidSubnets: []string{}, - }, - TTL: TTLVConfig{ - Max: mockMax, - Jitter: mockJitter, - Now: mockNow, - }, - } - buildNoneConfig = ValidatorConfig{ - URL: URLVConfig{ - HTTPSOnly: false, - AllowLoopback: true, - AllowIP: true, - AllowSpecialUseHosts: true, - AllowSpecialUseIPs: true, - InvalidHosts: []string{}, - InvalidSubnets: []string{}, - }, - TTL: TTLVConfig{ - Max: mockMax, - Jitter: mockJitter, - Now: mockNow, - }, - } -) - -func TestBuildValidURLFuncs(t *testing.T) { - tcs := []struct { - desc string - config ValidatorConfig - expectedErr error - expectedFuncCount int - }{ - // { - // desc: "HTTPSOnly only", - // config: ValidatorConfig{ - // URL: URLVConfig{ - // HTTPSOnly: true, - // AllowLoopback: true, - // AllowIP: true, - // AllowSpecialUseHosts: true, - // AllowSpecialUseIPs: true, - // }, - // }, - // expectedFuncCount: 1, - // }, - // { - // desc: "AllowLoopback only", - // config: ValidatorConfig{ - // URL: URLVConfig{ - // HTTPSOnly: false, - // AllowLoopback: false, - // AllowIP: true, - // AllowSpecialUseHosts: true, - // AllowSpecialUseIPs: true, - // }, - // }, - // expectedFuncCount: 2, - // }, - // { - // desc: "AllowIp Only", - // config: ValidatorConfig{ - // URL: URLVConfig{ - // HTTPSOnly: false, - // AllowLoopback: true, - // AllowIP: false, - // AllowSpecialUseHosts: true, - // AllowSpecialUseIPs: true, - // }, - // }, - // expectedFuncCount: 2, - // }, - // { - // desc: "AllowSpecialUseHosts Only", - // config: ValidatorConfig{ - // URL: URLVConfig{ - // HTTPSOnly: false, - // AllowLoopback: true, - // AllowIP: true, - // AllowSpecialUseHosts: false, - // AllowSpecialUseIPs: true, - // }, - // }, - // expectedFuncCount: 2, - // }, - // { - // desc: "AllowSpecialuseIPS Only", - // config: ValidatorConfig{ - // URL: URLVConfig{ - // HTTPSOnly: false, - // AllowLoopback: true, - // AllowIP: true, - // AllowSpecialUseHosts: true, - // AllowSpecialUseIPs: false, - // }, - // }, - // expectedFuncCount: 2, - // }, - { - desc: "InvalidSubnet Failure", - config: ValidatorConfig{ - URL: URLVConfig{ - HTTPSOnly: false, - AllowLoopback: true, - AllowIP: true, - AllowSpecialUseHosts: true, - AllowSpecialUseIPs: false, - InvalidSubnets: []string{"https://localhost:9000"}, - }, - }, - expectedErr: fmt.Errorf("error"), - }, - // { - // desc: "Build None", - // config: ValidatorConfig{ - // URL: buildNoneConfig.URL, - // }, - // expectedFuncCount: 1, - // }, - // { - // desc: "Build All", - // config: ValidatorConfig{ - // URL: buildAllConfig.URL, - // }, - // expectedFuncCount: 5, - // }, - } - for _, tc := range tcs { - t.Run(tc.desc, func(t *testing.T) { - assert := assert.New(t) - vals, err := tc.config.GetValidator() - if tc.expectedErr != nil { - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr)) - assert.Nil(vals) - return - } - require.NoError(t, err) - }) - } -} - -func TestBuildValidators(t *testing.T) { - tcs := []struct { - desc string - config ValidatorConfig - expectedErr error - expectedFuncCount int - }{ - { - desc: "BuildValidURLFuncs Failure", - config: ValidatorConfig{ - URL: URLVConfig{ - HTTPSOnly: false, - AllowLoopback: true, - AllowIP: true, - AllowSpecialUseHosts: true, - AllowSpecialUseIPs: false, - InvalidSubnets: []string{"https://localhost:9000"}, - }, - }, - expectedErr: fmt.Errorf("error"), - }, - { - desc: "CheckDuration Failure", - config: ValidatorConfig{ - TTL: TTLVConfig{ - Max: -1 * time.Second, - }, - }, - expectedErr: fmt.Errorf("error"), - }, - { - desc: "CheckUntil Failure", - config: ValidatorConfig{ - TTL: TTLVConfig{ - Jitter: -1 * time.Second, - }, - }, - expectedErr: fmt.Errorf("error"), - }, - { - desc: "All Validators Added", - expectedFuncCount: 8, - }, - } - for _, tc := range tcs { - t.Run(tc.desc, func(t *testing.T) { - assert := assert.New(t) - opts := BuildOptions(tc.config, nil) - assert.NotNil(opts) - }) - } -} diff --git a/webhook.go b/webhook.go index c0a3a4a..70c6349 100644 --- a/webhook.go +++ b/webhook.go @@ -13,9 +13,12 @@ import ( ) var ( - ErrInvalidInput = fmt.Errorf("invalid input") - ErrInvalidType = fmt.Errorf("invalid type") - ErrUknownType = fmt.Errorf("unknown type") + ErrInvalidInput = fmt.Errorf("invalid input") + ErrInvalidType = fmt.Errorf("invalid type") + ErrUknownType = fmt.Errorf("unknown type") + errInvalidTTL = errors.New("TTL must be non-negative") + errInvalidJitter = errors.New("jitter must be non-negative") + errInvalidUntil = errors.New("until value of webhook is out of bounds") ) // Deprecated: This substructure should only be used for backwards compatibility @@ -300,6 +303,30 @@ func (v1 *RegistrationV1) ValidateDuration(ttl time.Duration) error { return errs } +func (v1 *RegistrationV1) CheckUntil(now func() time.Time, jitter, maxTTL time.Duration) error { + if now == nil { + now = time.Now + } + + if maxTTL < 0 { + return errInvalidTTL + } else if jitter < 0 { + return errInvalidJitter + } + + if v1.Until.IsZero() { + return nil + } + limit := (now().Add(maxTTL)).Add(jitter) + proposed := (v1.Until) + if proposed.After(limit) { + return fmt.Errorf("%w: %v after %v", + errInvalidUntil, proposed.String(), limit.String()) + } + return nil + +} + func (v1 *RegistrationV1) ValidateReceiverURL(c *urlegit.Checker) error { if v1.Config.ReceiverURL != "" { if err := c.Text(v1.Config.ReceiverURL); err != nil { From 5b48a48d53cdfdf5c6178eb8f57cfb1edff5a948 Mon Sep 17 00:00:00 2001 From: Owen Cabalceta Date: Tue, 29 Oct 2024 14:50:29 -0400 Subject: [PATCH 45/45] chore: update based on pr feedback --- webhook.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/webhook.go b/webhook.go index 70c6349..d7cb3f3 100644 --- a/webhook.go +++ b/webhook.go @@ -75,6 +75,7 @@ type RegistrationV1 struct { nowFunc func() time.Time `json:"-"` } +// RetryHint is the substructure for configuration related to retrying requests. type RetryHint struct { //RetryEachUrl is the amount of times a URL should be retried given a failed response until the next URL in the request is tried. //Default value will be set to none @@ -84,6 +85,16 @@ type RetryHint struct { MaxRetry int `json:"max_retry"` } +// DNSSrvRecord is the substructure for configuration related to load balancing. +type DNSSrvRecord struct { + // FQDNs is a list of FQDNs pointing to dns srv records + FQDNs []string `json:"fqdns"` + + // LoadBalancingScheme is the scheme to use for load balancing. Either the + // srv record attribute `weight` or `priortiy` can be used. + LoadBalancingScheme string `json:"load_balancing_scheme"` +} + // Webhook is a substructure with data related to event delivery. type Webhook struct { // Accept is the encoding type of outgoing events. The following encoding types are supported, otherwise @@ -118,14 +129,7 @@ type Webhook struct { // DNSSrvRecord is the substructure for configuration related to load balancing. // Note: either `ReceiverURLs` or `DNSSrvRecord` must be used but not both. - DNSSrvRecord struct { - // FQDNs is a list of FQDNs pointing to dns srv records - FQDNs []string `json:"fqdns"` - - // LoadBalancingScheme is the scheme to use for load balancing. Either the - // srv record attribute `weight` or `priortiy` can be used. - LoadBalancingScheme string `json:"load_balancing_scheme"` - } `json:"dns_srv_record"` + DNSSrvRecord DNSSrvRecord `json:"dns_srv_record"` //RetryHint is the substructure for configuration related to retrying requests. // (Optional, if omited then retries will be based on default values defined by server) @@ -134,7 +138,11 @@ type Webhook struct { // Kafka is a substructure with data related to event delivery. type Kafka struct { - // Accept is content type value to set WRP messages to (unless already specified in the WRP). + // Accept is the encoding type of outgoing events. The following encoding types are supported, otherwise + // a 406 response code is returned: application/octet-stream, application/json, application/jsonl, application/msgpack. + // Note: An `Accept` of application/octet-stream or application/json will result in a single response for batch sizes of 0 or 1 + // and batch sizes greater than 1 will result in a multipart response. An `Accept` of application/jsonl or application/msgpack + // will always result in a single response with a list of batched events for any batch size. Accept string `json:"accept"` // BootstrapServers is a list of kafka broker addresses.