Skip to content

Commit 84f3677

Browse files
[receiver/webhook] Option to add a required header (#24452)
**Description:** Adding a feature - Allow option of adding a required header for incoming webhook requests. If header doesn't match, returns a 401. **Link to tracking Issue:** [<24270>](#24270)
1 parent d1937d6 commit 84f3677

File tree

7 files changed

+111
-9
lines changed

7 files changed

+111
-9
lines changed

.chloggen/webhook-require-header.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Use this changelog template to create an entry for release notes.
2+
# If your change doesn't affect end users, such as a test fix or a tooling change,
3+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
4+
5+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
6+
change_type: enhancement
7+
8+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
9+
component: webhookreceiver
10+
11+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
12+
note: "Add an optional config setting to set a required header that all incoming requests must provide"
13+
14+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
15+
issues: [24270]
16+
17+
# (Optional) One or more lines of additional information to render under the primary note.
18+
# These lines will be padded with 2 spaces and then inserted directly into the document.
19+
# Use pipe (|) for multiline entries.
20+
subtext:

receiver/webhookeventreceiver/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ The following settings are optional:
2727
* `health_path` (default: '/health_check'): Path available for checking receiver status
2828
* `read_timeout` (default: '500ms'): Maximum wait time while attempting to read a received event
2929
* `write_timeout` (default: '500ms'): Maximum wait time while attempting to write a response
30+
* `required_header` (optional):
31+
* `key` (required if `required_header` config option is set): Represents the key portion of the required header.
32+
* `value` (required if `required_header` config option is set): Represents the value portion of the required header.
3033

3134
Example:
3235
```yaml
@@ -36,6 +39,9 @@ receivers:
3639
read_timeout: "500ms"
3740
path: "eventsource/receiver"
3841
health_path: "eventreceiver/healthcheck"
42+
required_header:
43+
key: "required-header-key"
44+
value: "required-header-value"
3945
```
4046
The full list of settings exposed for this receiver are documented [here](./config.go) with a detailed sample configuration [here](./testdata/config.yaml)
4147

receiver/webhookeventreceiver/config.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,22 @@ var (
1515
errMissingEndpointFromConfig = errors.New("missing receiver server endpoint from config")
1616
errReadTimeoutExceedsMaxValue = errors.New("The duration specified for read_timeout exceeds the maximum allowed value of 10s")
1717
errWriteTimeoutExceedsMaxValue = errors.New("The duration specified for write_timeout exceeds the maximum allowed value of 10s")
18+
errRequiredHeader = errors.New("both key and value are required to assign a required_header")
1819
)
1920

2021
// Config defines configuration for the Generic Webhook receiver.
2122
type Config struct {
2223
confighttp.HTTPServerSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct
23-
ReadTimeout string `mapstructure:"read_timeout"` // wait time for reading request headers in ms. Default is twenty seconds.
24-
WriteTimeout string `mapstructure:"write_timeout"` // wait time for writing request response in ms. Default is twenty seconds.
25-
Path string `mapstructure:"path"` // path for data collection. Default is <host>:<port>/services/collector
26-
HealthPath string `mapstructure:"health_path"` // path for health check api. Default is /services/collector/health
24+
ReadTimeout string `mapstructure:"read_timeout"` // wait time for reading request headers in ms. Default is twenty seconds.
25+
WriteTimeout string `mapstructure:"write_timeout"` // wait time for writing request response in ms. Default is twenty seconds.
26+
Path string `mapstructure:"path"` // path for data collection. Default is <host>:<port>/services/collector
27+
HealthPath string `mapstructure:"health_path"` // path for health check api. Default is /services/collector/health
28+
RequiredHeader RequiredHeader `mapstructure:"required_header"` // optional setting to set a required header for all requests to have
29+
}
30+
31+
type RequiredHeader struct {
32+
Key string `mapstructure:"key"`
33+
Value string `mapstructure:"value"`
2734
}
2835

2936
func (cfg *Config) Validate() error {
@@ -59,5 +66,9 @@ func (cfg *Config) Validate() error {
5966
}
6067
}
6168

69+
if (cfg.RequiredHeader.Key != "" && cfg.RequiredHeader.Value == "") || (cfg.RequiredHeader.Value != "" && cfg.RequiredHeader.Key == "") {
70+
errs = multierr.Append(errs, errRequiredHeader)
71+
}
72+
6273
return errs
6374
}

receiver/webhookeventreceiver/config_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func TestValidateConfig(t *testing.T) {
2424
errs = multierr.Append(errs, errMissingEndpointFromConfig)
2525
errs = multierr.Append(errs, errReadTimeoutExceedsMaxValue)
2626
errs = multierr.Append(errs, errWriteTimeoutExceedsMaxValue)
27+
errs = multierr.Append(errs, errRequiredHeader)
2728

2829
tests := []struct {
2930
desc string
@@ -59,6 +60,32 @@ func TestValidateConfig(t *testing.T) {
5960
WriteTimeout: "14s",
6061
},
6162
},
63+
{
64+
desc: "RequiredHeader does not contain both a key and a value",
65+
expect: errRequiredHeader,
66+
conf: Config{
67+
HTTPServerSettings: confighttp.HTTPServerSettings{
68+
Endpoint: "",
69+
},
70+
RequiredHeader: RequiredHeader{
71+
Key: "key-present",
72+
Value: "",
73+
},
74+
},
75+
},
76+
{
77+
desc: "RequiredHeader does not contain both a key and a value",
78+
expect: errRequiredHeader,
79+
conf: Config{
80+
HTTPServerSettings: confighttp.HTTPServerSettings{
81+
Endpoint: "",
82+
},
83+
RequiredHeader: RequiredHeader{
84+
Key: "",
85+
Value: "value-present",
86+
},
87+
},
88+
},
6289
{
6390
desc: "Multiple invalid configs",
6491
expect: errs,
@@ -68,6 +95,10 @@ func TestValidateConfig(t *testing.T) {
6895
},
6996
WriteTimeout: "14s",
7097
ReadTimeout: "15s",
98+
RequiredHeader: RequiredHeader{
99+
Key: "",
100+
Value: "value-present",
101+
},
71102
},
72103
},
73104
}
@@ -99,6 +130,10 @@ func TestLoadConfig(t *testing.T) {
99130
WriteTimeout: "500ms",
100131
Path: "some/path",
101132
HealthPath: "health/path",
133+
RequiredHeader: RequiredHeader{
134+
Key: "key-present",
135+
Value: "value-present",
136+
},
102137
}
103138

104139
// create expected config

receiver/webhookeventreceiver/receiver.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ import (
2525
)
2626

2727
var (
28-
errNilLogsConsumer = errors.New("missing a logs consumer")
29-
errMissingEndpoint = errors.New("missing a receiver endpoint")
30-
errInvalidRequestMethod = errors.New("invalid method. Valid method is POST")
31-
errInvalidEncodingType = errors.New("invalid encoding type")
32-
errEmptyResponseBody = errors.New("request body content length is zero")
28+
errNilLogsConsumer = errors.New("missing a logs consumer")
29+
errMissingEndpoint = errors.New("missing a receiver endpoint")
30+
errInvalidRequestMethod = errors.New("invalid method. Valid method is POST")
31+
errInvalidEncodingType = errors.New("invalid encoding type")
32+
errEmptyResponseBody = errors.New("request body content length is zero")
33+
errMissingRequiredHeader = errors.New("request was missing required header or incorrect header value")
3334
)
3435

3536
const healthyResponse = `{"text": "Webhookevent receiver is healthy"}`
@@ -153,6 +154,14 @@ func (er *eventReceiver) handleReq(w http.ResponseWriter, r *http.Request, _ htt
153154
return
154155
}
155156

157+
if er.cfg.RequiredHeader.Key != "" {
158+
requiredHeaderValue := r.Header.Get(er.cfg.RequiredHeader.Key)
159+
if requiredHeaderValue != er.cfg.RequiredHeader.Value {
160+
er.failBadReq(ctx, w, http.StatusUnauthorized, errMissingRequiredHeader)
161+
return
162+
}
163+
}
164+
156165
encoding := r.Header.Get("Content-Encoding")
157166
// only support gzip if encoding header is set.
158167
if encoding != "" && encoding != "gzip" {

receiver/webhookeventreceiver/receiver_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ func TestCreateNewLogReceiver(t *testing.T) {
4848
WriteTimeout: "210",
4949
Path: "/event",
5050
HealthPath: "/health",
51+
RequiredHeader: RequiredHeader{
52+
Key: "key-present",
53+
Value: "value-present",
54+
},
5155
},
5256
consumer: consumertest.NewNop(),
5357
},
@@ -147,6 +151,10 @@ func TestHandleReq(t *testing.T) {
147151
func TestFailedReq(t *testing.T) {
148152
cfg := createDefaultConfig().(*Config)
149153
cfg.Endpoint = "localhost:0"
154+
headerCfg := createDefaultConfig().(*Config)
155+
headerCfg.Endpoint = "localhost:0"
156+
headerCfg.RequiredHeader.Key = "key-present"
157+
headerCfg.RequiredHeader.Value = "value-present"
150158

151159
tests := []struct {
152160
desc string
@@ -186,6 +194,16 @@ func TestFailedReq(t *testing.T) {
186194
}(),
187195
status: http.StatusBadRequest,
188196
},
197+
{
198+
desc: "Invalid required header value",
199+
cfg: *headerCfg,
200+
req: func() *http.Request {
201+
req := httptest.NewRequest("POST", "http://localhost/events", strings.NewReader("test"))
202+
req.Header.Set("key-present", "incorrect-value")
203+
return req
204+
}(),
205+
status: http.StatusUnauthorized,
206+
},
189207
}
190208
for _, test := range tests {
191209
t.Run(test.desc, func(t *testing.T) {

receiver/webhookeventreceiver/testdata/config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ webhookevent/valid_config:
55
write_timeout: "500ms"
66
path: "some/path"
77
health_path: "health/path"
8+
required_header:
9+
key: key-present
10+
value: value-present

0 commit comments

Comments
 (0)