Skip to content

Commit 1bf3b1f

Browse files
borismattijssenadamyeats
authored andcommitted
Make prefix whitelist user-definable
1 parent 1e36c68 commit 1bf3b1f

File tree

12 files changed

+138
-37
lines changed

12 files changed

+138
-37
lines changed

pkg/plugin/driver.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ import (
3030
// Clickhouse defines how to connect to a Clickhouse datasource
3131
type Clickhouse struct{}
3232

33-
var headersAsLogCommentPrefixWhitelist = []string{"X-Dashboard", "X-Panel", "X-Rule"}
34-
3533
// getTLSConfig returns tlsConfig from settings
3634
// logic reused from https://github.com/grafana/grafana/blob/615c153b3a2e4d80cff263e67424af6edb992211/pkg/models/datasource_cache.go#L211
3735
func getTLSConfig(settings Settings) (*tls.Config, error) {
@@ -187,7 +185,7 @@ func (h *Clickhouse) Connect(ctx context.Context, config backend.DataSourceInsta
187185
}
188186

189187
if settings.LogHeadersAsComment {
190-
logComment, err := headersToLogComment(httpHeaders)
188+
logComment, err := headersToLogComment(httpHeaders, settings.LogHeadersAsCommentRegex)
191189
if err != nil {
192190
backend.Logger.Warn("Failed to serialize headers as JSON", "error", err)
193191
} else {
@@ -493,10 +491,11 @@ func extractForwardedHeadersFromMessage(message json.RawMessage) (map[string]str
493491
}
494492

495493
// headersToLogComment serializes the headers to a JSON string for use as a log comment.
496-
func headersToLogComment(headers map[string]string) (string, error) {
494+
func headersToLogComment(headers map[string]string, regexPattern *regexp.Regexp) (string, error) {
495+
// Compile the regex pattern
497496
whiteListedHeaders := make(map[string]string)
498497
for k, v := range headers {
499-
if hasAnyPrefixCaseInsensitive(k, headersAsLogCommentPrefixWhitelist) {
498+
if regexPattern.MatchString(k) {
500499
whiteListedHeaders[k] = v
501500
}
502501
}
@@ -507,15 +506,6 @@ func headersToLogComment(headers map[string]string) (string, error) {
507506
return string(headersJSON), nil
508507
}
509508

510-
func hasAnyPrefixCaseInsensitive(s string, prefixes []string) bool {
511-
for _, prefix := range prefixes {
512-
if strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
513-
return true
514-
}
515-
}
516-
return false
517-
}
518-
519509
func mergeOpenTelemetryLabels(frame *data.Frame) error {
520510
var attrFields []*data.Field
521511
for _, field := range frame.Fields {

pkg/plugin/driver_test.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"regexp"
78
"testing"
89

910
"github.com/ClickHouse/clickhouse-go/v2"
@@ -318,22 +319,9 @@ func TestContainsClickHouseException(t *testing.T) {
318319
})
319320
}
320321

321-
func TestHasAnyPrefixCaseInsensitive(t *testing.T) {
322-
prefixes := []string{"X-Dashboard", "X-Panel", "X-Rule"}
323-
324-
t.Run("matches case-insensitive prefix", func(t *testing.T) {
325-
assert.True(t, hasAnyPrefixCaseInsensitive("X-Dashboard-123", prefixes))
326-
assert.True(t, hasAnyPrefixCaseInsensitive("x-dashboard-123", prefixes))
327-
assert.True(t, hasAnyPrefixCaseInsensitive("X-PANEL-456", prefixes))
328-
})
329-
330-
t.Run("does not match non-whitelisted prefix", func(t *testing.T) {
331-
assert.False(t, hasAnyPrefixCaseInsensitive("X-Other-123", prefixes))
332-
assert.False(t, hasAnyPrefixCaseInsensitive("X-Grafana-Id", prefixes))
333-
})
334-
}
335-
336322
func TestHeadersToLogComment(t *testing.T) {
323+
regexPattern := "(?i)^(x-dashboard|x-panel|x-rule)"
324+
337325
t.Run("filters and serializes whitelisted headers", func(t *testing.T) {
338326
headers := map[string]string{
339327
"X-Dashboard-Id": "123",
@@ -342,7 +330,7 @@ func TestHeadersToLogComment(t *testing.T) {
342330
"Authorization": "should-be-excluded",
343331
}
344332

345-
result, err := headersToLogComment(headers)
333+
result, err := headersToLogComment(headers, regexp.MustCompile(regexPattern))
346334
assert.NoError(t, err)
347335

348336
var resultMap map[string]string
@@ -361,7 +349,7 @@ func TestHeadersToLogComment(t *testing.T) {
361349
"Authorization": "token",
362350
}
363351

364-
result, err := headersToLogComment(headers)
352+
result, err := headersToLogComment(headers, regexp.MustCompile(regexPattern))
365353
assert.NoError(t, err)
366354
assert.Equal(t, "{}", result)
367355
})

pkg/plugin/settings.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"regexp"
78
"strconv"
89
"strings"
910
"time"
@@ -40,11 +41,12 @@ type Settings struct {
4041
MaxIdleConns string `json:"maxIdleConns,omitempty"`
4142
MaxOpenConns string `json:"maxOpenConns,omitempty"`
4243

43-
HttpHeaders map[string]string `json:"-"`
44-
ForwardGrafanaHeaders bool `json:"forwardGrafanaHeaders,omitempty"`
45-
LogHeadersAsComment bool `json:"logHeadersAsComment,omitempty"`
46-
CustomSettings []CustomSetting `json:"customSettings"`
47-
ProxyOptions *proxy.Options
44+
HttpHeaders map[string]string `json:"-"`
45+
ForwardGrafanaHeaders bool `json:"forwardGrafanaHeaders,omitempty"`
46+
LogHeadersAsComment bool `json:"logHeadersAsComment,omitempty"`
47+
LogHeadersAsCommentRegex *regexp.Regexp `json:"logHeadersAsCommentRegex,omitempty"`
48+
CustomSettings []CustomSetting `json:"customSettings"`
49+
ProxyOptions *proxy.Options
4850

4951
RowLimit int64 `json:"rowLimit,omitempty"`
5052
EnableRowLimit bool `json:"enableRowLimit,omitempty"`
@@ -197,6 +199,17 @@ func LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings
197199
settings.LogHeadersAsComment = jsonData["logHeadersAsComment"].(bool)
198200
}
199201
}
202+
if jsonData["logHeadersAsCommentRegex"] != nil {
203+
if regex, ok := jsonData["logHeadersAsCommentRegex"].(string); ok {
204+
settings.LogHeadersAsCommentRegex, err = regexp.Compile(regex)
205+
if err != nil {
206+
return settings, backend.DownstreamError(fmt.Errorf("could not parse logHeadersAsCommentRegex value: %w", err))
207+
}
208+
}
209+
}
210+
if settings.LogHeadersAsComment && settings.LogHeadersAsCommentRegex == nil {
211+
return settings, backend.DownstreamError(fmt.Errorf("logHeadersAsCommentRegex is required when logHeadersAsComment is true"))
212+
}
200213

201214
if jsonData["enableRowLimit"] != nil {
202215
if enableRowLimitString, ok := jsonData["enableRowLimit"].(string); ok {

src/components/configEditor/HttpHeadersConfig.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('HttpHeadersConfig', () => {
1616
onHttpHeadersChange={() => {}}
1717
onForwardGrafanaHeadersChange={() => {}}
1818
onLogHeadersAsCommentChange={() => {}}
19+
onLogHeadersAsCommentRegexChange={() => {}}
1920
/>
2021
);
2122
expect(result.container.firstChild).not.toBeNull();
@@ -31,6 +32,7 @@ describe('HttpHeadersConfig', () => {
3132
onHttpHeadersChange={onHttpHeadersChange}
3233
onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}
3334
onLogHeadersAsCommentChange={() => {}}
35+
onLogHeadersAsCommentRegexChange={() => {}}
3436
/>
3537
);
3638
expect(result.container.firstChild).not.toBeNull();
@@ -52,6 +54,7 @@ describe('HttpHeadersConfig', () => {
5254
onHttpHeadersChange={onHttpHeadersChange}
5355
onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}
5456
onLogHeadersAsCommentChange={() => {}}
57+
onLogHeadersAsCommentRegexChange={() => {}}
5558
/>
5659
);
5760
expect(result.container.firstChild).not.toBeNull();
@@ -102,6 +105,7 @@ describe('HttpHeadersConfig', () => {
102105
onHttpHeadersChange={onHttpHeadersChange}
103106
onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}
104107
onLogHeadersAsCommentChange={() => {}}
108+
onLogHeadersAsCommentRegexChange={() => {}}
105109
/>
106110
);
107111
expect(result.container.firstChild).not.toBeNull();
@@ -149,6 +153,7 @@ describe('forwardGrafanaHTTPHeaders', () => {
149153
onHttpHeadersChange={onHttpHeadersChange}
150154
onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}
151155
onLogHeadersAsCommentChange={() => {}}
156+
onLogHeadersAsCommentRegexChange={() => {}}
152157
/>
153158
);
154159
expect(result.container.firstChild).not.toBeNull();
@@ -174,6 +179,7 @@ describe('logHeadersAsComment', () => {
174179
onHttpHeadersChange={onHttpHeadersChange}
175180
onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}
176181
onLogHeadersAsCommentChange={onLogHeadersAsCommentChange}
182+
onLogHeadersAsCommentRegexChange={() => {}}
177183
/>
178184
);
179185
expect(result.container.firstChild).not.toBeNull();
@@ -185,4 +191,32 @@ describe('logHeadersAsComment', () => {
185191
expect(onLogHeadersAsCommentChange).toHaveBeenCalledWith(true);
186192
});
187193

194+
it('should call onLogHeadersAsCommentRegexChange when input is changed', () => {
195+
const onHttpHeadersChange = jest.fn();
196+
const onForwardGrafanaHeadersChange = jest.fn();
197+
const onLogHeadersAsCommentChange = jest.fn();
198+
const onLogHeadersAsCommentRegexChange = jest.fn();
199+
const result = render(
200+
<HttpHeadersConfig
201+
headers={[]}
202+
secureFields={{}}
203+
logHeadersAsComment={true}
204+
onHttpHeadersChange={onHttpHeadersChange}
205+
onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}
206+
onLogHeadersAsCommentChange={onLogHeadersAsCommentChange}
207+
onLogHeadersAsCommentRegexChange={onLogHeadersAsCommentRegexChange}
208+
/>
209+
);
210+
expect(result.container.firstChild).not.toBeNull();
211+
212+
const logHeadersAsCommentRegexInput = result.getByTestId(selectors.logHeadersAsCommentRegexInput);
213+
expect(logHeadersAsCommentRegexInput).toBeInTheDocument();
214+
fireEvent.change(logHeadersAsCommentRegexInput, { target: { value: 'test' } });
215+
expect(onLogHeadersAsCommentRegexChange).toHaveBeenCalledTimes(1);
216+
expect(onLogHeadersAsCommentRegexChange).toHaveBeenCalledWith(
217+
expect.objectContaining({
218+
target: expect.objectContaining({ value: 'test' }),
219+
})
220+
);
221+
});
188222
});

src/components/configEditor/HttpHeadersConfig.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ interface HttpHeadersConfigProps {
1111
headers?: CHHttpHeader[];
1212
forwardGrafanaHeaders?: boolean;
1313
logHeadersAsComment?: boolean;
14+
logHeadersAsCommentRegex?: string;
1415
secureFields: KeyValue<boolean>;
1516
onHttpHeadersChange: (v: CHHttpHeader[]) => void;
1617
onForwardGrafanaHeadersChange: (v: boolean) => void;
1718
onLogHeadersAsCommentChange: (v: boolean) => void;
19+
onLogHeadersAsCommentRegexChange: (e: ChangeEvent<HTMLInputElement>) => void;
1820
}
1921

2022
export const HttpHeadersConfig = (props: HttpHeadersConfigProps) => {
@@ -23,6 +25,9 @@ export const HttpHeadersConfig = (props: HttpHeadersConfigProps) => {
2325
const [headers, setHeaders] = useState<CHHttpHeader[]>(props.headers || []);
2426
const [forwardGrafanaHeaders, setForwardGrafanaHeaders] = useState<boolean>(props.forwardGrafanaHeaders || false);
2527
const [logHeadersAsComment, setLogHeadersAsComment] = useState<boolean>(props.logHeadersAsComment || false);
28+
const [logHeadersAsCommentRegex, setLogHeadersAsCommentRegex] = useState<string>(
29+
props.logHeadersAsCommentRegex || allLabels.components.Config.HttpHeadersConfig.logHeadersAsCommentRegex.placeholder
30+
);
2631
const labels = allLabels.components.Config.HttpHeadersConfig;
2732
const selectors = allSelectors.components.Config.HttpHeaderConfig;
2833

@@ -49,6 +54,11 @@ export const HttpHeadersConfig = (props: HttpHeadersConfigProps) => {
4954
props.onLogHeadersAsCommentChange(value);
5055
};
5156

57+
const updateLogHeadersAsCommentRegex = (e: ChangeEvent<HTMLInputElement>) => {
58+
setLogHeadersAsCommentRegex(e.target.value);
59+
props.onLogHeadersAsCommentRegexChange(e);
60+
};
61+
5262
return (
5363
<ConfigSection title={labels.title}>
5464
<Field label={labels.label} description={labels.description}>
@@ -92,6 +102,19 @@ export const HttpHeadersConfig = (props: HttpHeadersConfigProps) => {
92102
onChange={(e) => updateLogHeadersAsComment(e.currentTarget.checked)}
93103
/>
94104
</Field>
105+
{logHeadersAsComment && (
106+
<Field
107+
label={labels.logHeadersAsCommentRegex.label}
108+
description={labels.logHeadersAsCommentRegex.tooltip}
109+
>
110+
<Input
111+
data-testid={selectors.logHeadersAsCommentRegexInput}
112+
value={logHeadersAsCommentRegex}
113+
placeholder={labels.logHeadersAsCommentRegex.placeholder}
114+
onChange={(e: ChangeEvent<HTMLInputElement>) => updateLogHeadersAsCommentRegex(e)}
115+
/>
116+
</Field>
117+
)}
95118
</ConfigSection>
96119
);
97120
};

src/labels.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ export default {
9595
label: 'Log Headers as Comment',
9696
tooltip: 'Serialize Grafana HTTP headers as JSON and include them as a log comment with each query.',
9797
},
98+
logHeadersAsCommentRegex: {
99+
label: 'Header Whitelist Regex',
100+
tooltip: `Regular expression to match headers that should be logged.`,
101+
placeholder: '(?i)^(x-dashboard|x-panel|x-rule)',
102+
},
98103
},
99104
AliasTableConfig: {
100105
title: 'Column Alias Tables',

src/selectors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const Components = {
123123
headerValueInput: 'config__http-header-config__header-value-input',
124124
forwardGrafanaHeadersSwitch: 'config__http-header-config__forward-grafana-headers-switch',
125125
logHeadersAsCommentSwitch: 'config__http-header-config__log-headers-as-comment-switch',
126+
logHeadersAsCommentRegexInput: 'config__http-header-config__log-headers-as-comment-regex-input',
126127
},
127128
AliasTableConfig: {
128129
aliasEditor: 'config__alias-table-config__alias-editor',

src/types/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface CHConfig extends DataSourceJsonData {
3838
httpHeaders?: CHHttpHeader[];
3939
forwardGrafanaHeaders?: boolean;
4040
logHeadersAsComment?: boolean;
41+
logHeadersAsCommentRegex?: string;
4142

4243
customSettings?: CHCustomSetting[];
4344
enableSecureSocksProxy?: boolean;

src/views/CHConfigEditor.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ export const ConfigEditor: React.FC<ConfigEditorProps> = (props) => {
315315
headers={options.jsonData.httpHeaders}
316316
forwardGrafanaHeaders={options.jsonData.forwardGrafanaHeaders}
317317
logHeadersAsComment={options.jsonData.logHeadersAsComment}
318+
logHeadersAsCommentRegex={options.jsonData.logHeadersAsCommentRegex}
318319
secureFields={options.secureJsonFields}
319320
onHttpHeadersChange={(headers) => onHttpHeadersChange(headers, options, onOptionsChange)}
320321
onForwardGrafanaHeadersChange={(forwardGrafanaHeaders) =>
@@ -323,6 +324,9 @@ export const ConfigEditor: React.FC<ConfigEditorProps> = (props) => {
323324
onLogHeadersAsCommentChange={(logHeadersAsComment) =>
324325
onSwitchToggle('logHeadersAsComment', logHeadersAsComment)
325326
}
327+
onLogHeadersAsCommentRegexChange={(e) => {
328+
onUpdateDatasourceJsonDataOption(props, 'logHeadersAsCommentRegex')(e);
329+
}}
326330
/>
327331
)}
328332

src/views/config-v2/HttpHeadersConfigV2.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ describe('HttpHeadersConfigV2', () => {
130130
);
131131
});
132132

133+
it('should call onLogHeadersAsCommentRegexChange when input is changed', () => {
134+
renderWith({ logHeadersAsComment: true, logHeadersAsCommentRegex: '' });
135+
136+
const logHeadersAsCommentRegexInput = screen.getByTestId(selectors.components.Config.HttpHeaderConfig.logHeadersAsCommentRegexInput);
137+
expect(logHeadersAsCommentRegexInput).toBeInTheDocument();
138+
fireEvent.change(logHeadersAsCommentRegexInput, { target: { value: 'test' } });
139+
expect(onOptionsChangeMock).toHaveBeenCalledTimes(1);
140+
expect(onOptionsChangeMock).toHaveBeenLastCalledWith(
141+
expect.objectContaining({
142+
jsonData: expect.objectContaining({ logHeadersAsCommentRegex: 'test' }),
143+
})
144+
);
145+
});
146+
133147
describe('HttpHeadersConfigV2', () => {
134148
const onHttpHeadersChange = jest.fn();
135149
const onOptionsChangeMock = jest.fn();

0 commit comments

Comments
 (0)