From 80c30f1879bcd2be050f665709d55bec1e8758cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 26 Sep 2019 17:18:27 +0100 Subject: [PATCH] fix(ui): pass configured alertmanager headers when making requests from the browser Right now configured headers are only set on requests made from the backend to alertmanager API. With this change fetch() calls in the browser will use those headers if proxy mode is not enabled. --- ui/package.json | 1 + ui/src/Common/Fetch.js | 6 +++ ui/src/Common/Fetch.test.js | 40 +++++++++++++++++++ .../AlertGroup/Silence/DeleteSilence.js | 7 ++-- .../AlertGroup/Silence/DeleteSilence.test.js | 31 +++++++++++++- .../AlertGroup/Silence/index.test.js | 2 + .../MainModal/Configuration/SortLabelName.js | 8 ++-- ui/src/Components/NavBar/FilterInput/index.js | 5 ++- .../AlertManagerInput/index.test.js | 3 ++ .../SilenceMatch/LabelNameInput.js | 14 +++---- .../SilenceModal/SilenceMatch/MatchCounter.js | 3 +- .../SilenceModal/SilencePreview/index.js | 3 +- .../SilenceSubmit/SilenceSubmitProgress.js | 9 +++-- .../SilenceSubmitProgress.test.js | 6 ++- ui/src/Models/API.js | 1 + ui/src/Stores/AlertStore.js | 4 +- ui/src/__mocks__/Alerts.js | 3 ++ 17 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 ui/src/Common/Fetch.js create mode 100644 ui/src/Common/Fetch.test.js diff --git a/ui/package.json b/ui/package.json index a78b13e76..f64c53f1e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,6 +19,7 @@ "favico.js": "0.3.10", "fontfaceobserver": "2.1.0", "lodash.debounce": "4.0.8", + "lodash.merge": "4.6.2", "lodash.throttle": "4.1.1", "lodash.uniqueid": "4.0.1", "mobx": "5.13.0", diff --git a/ui/src/Common/Fetch.js b/ui/src/Common/Fetch.js new file mode 100644 index 000000000..15a1e2e11 --- /dev/null +++ b/ui/src/Common/Fetch.js @@ -0,0 +1,6 @@ +import merge from "lodash.merge"; + +const FetchWithCredentials = async (uri, options) => + await fetch(uri, merge({}, { credentials: "include" }, options)); + +export { FetchWithCredentials }; diff --git a/ui/src/Common/Fetch.test.js b/ui/src/Common/Fetch.test.js new file mode 100644 index 000000000..14483d44c --- /dev/null +++ b/ui/src/Common/Fetch.test.js @@ -0,0 +1,40 @@ +import { FetchWithCredentials } from "./Fetch"; + +beforeEach(() => { + fetch.resetMocks(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("FetchWithCredentials", () => { + it("fetch passes '{credentials: include}' to all requests", async () => { + const request = FetchWithCredentials("http://example.com", {}); + await expect(request).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith("http://example.com", { + credentials: "include" + }); + }); + + it("custom keys are merged with defaults", async () => { + const request = FetchWithCredentials("http://example.com", { + foo: "bar" + }); + await expect(request).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith("http://example.com", { + credentials: "include", + foo: "bar" + }); + }); + + it("custom credentials are used when passed", async () => { + const request = FetchWithCredentials("http://example.com", { + credentials: "none" + }); + await expect(request).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith("http://example.com", { + credentials: "none" + }); + }); +}); diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js index 055dbbc58..d22aa16e8 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.js @@ -15,6 +15,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch"; import { APIAlertmanagerUpstream } from "Models/API"; import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query"; +import { FetchWithCredentials } from "Common/Fetch"; import { Modal } from "Components/Modal"; import { LabelSetList, @@ -136,7 +137,7 @@ const DeleteSilenceModalContent = observer( FormatQuery(StaticLabels.SilenceID, QueryOperators.Equal, silenceID) ]); - this.previewState.fetch = fetch(alertsURI, { credentials: "include" }) + this.previewState.fetch = FetchWithCredentials(alertsURI, {}) .then(result => result.json()) .then(result => { this.previewState.groupsToUniqueLabels(Object.values(result.groups)); @@ -165,9 +166,9 @@ const DeleteSilenceModalContent = observer( ? `${alertmanager.publicURI}/api/v2/silence/${silenceID}` : `${alertmanager.publicURI}/api/v1/silence/${silenceID}`; - this.deleteState.fetch = fetch(uri, { + this.deleteState.fetch = FetchWithCredentials(uri, { method: "DELETE", - credentials: "include" + headers: alertmanager.headers }) .then(result => { if (isOpenAPI) { diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js index 7704ff8b2..f74f47a93 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/DeleteSilence.test.js @@ -122,7 +122,7 @@ describe("", () => { expect(tree.find("ErrorMessage")).toHaveLength(1); }); - it("[v1] sends a DELETE request after clicking 'Confirm' button ", async () => { + it("[v1] sends a DELETE request after clicking 'Confirm' button", async () => { await VerifyResponse({ status: "success" }); expect(fetch.mock.calls[1][0]).toBe( "http://am.example.com/api/v1/silence/123456789" @@ -130,7 +130,7 @@ describe("", () => { expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" }); }); - it("[v2] sends a DELETE request after clicking 'Confirm' button ", async () => { + it("[v2] sends a DELETE request after clicking 'Confirm' button", async () => { alertmanager.version = "0.16.2"; await VerifyResponse({ status: "success" }); expect(fetch.mock.calls[1][0]).toBe( @@ -139,6 +139,33 @@ describe("", () => { expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" }); }); + it("[v1] sends headers from alertmanager config", async () => { + alertmanager.headers = { Authorization: "Basic ***" }; + await VerifyResponse({ status: "success" }); + expect(fetch.mock.calls[1][0]).toBe( + "http://am.example.com/api/v1/silence/123456789" + ); + expect(fetch.mock.calls[1][1]).toMatchObject({ + credentials: "include", + method: "DELETE", + headers: { Authorization: "Basic ***" } + }); + }); + + it("[v1] sends headers from alertmanager config", async () => { + alertmanager.headers = { Authorization: "Basic ***" }; + alertmanager.version = "0.16.2"; + await VerifyResponse({ status: "success" }); + expect(fetch.mock.calls[1][0]).toBe( + "http://am.example.com/api/v2/silence/123456789" + ); + expect(fetch.mock.calls[1][1]).toMatchObject({ + credentials: "include", + method: "DELETE", + headers: { Authorization: "Basic ***" } + }); + }); + it("'Confirm' button is no-op after successful DELETE", async () => { const tree = await VerifyResponse({ status: "success" }); expect(fetch.mock.calls[1][0]).toBe( diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js index 1270712d6..51630994e 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js @@ -67,6 +67,7 @@ beforeEach(() => { cluster: "default", uri: "file:///mock", publicURI: "http://example.com", + headers: {}, error: "", version: "0.15.0", clusterMembers: ["default"] @@ -203,6 +204,7 @@ describe("", () => { cluster: "default", uri: "file:///mock", publicURI: "http://example.com", + headers: {}, error: "", version: "0.15.0", clusterMembers: ["default"] diff --git a/ui/src/Components/MainModal/Configuration/SortLabelName.js b/ui/src/Components/MainModal/Configuration/SortLabelName.js index 02c44cdf4..096b3f6aa 100644 --- a/ui/src/Components/MainModal/Configuration/SortLabelName.js +++ b/ui/src/Components/MainModal/Configuration/SortLabelName.js @@ -7,6 +7,7 @@ import { observer } from "mobx-react"; import Creatable from "react-select/creatable"; import { StaticLabels } from "Common/Query"; +import { FetchWithCredentials } from "Common/Fetch"; import { FormatBackendURI } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { ReactSelectStyles } from "Components/MultiSelect"; @@ -33,9 +34,10 @@ const SortLabelName = observer( }); populateNameSuggestions = action(() => { - this.nameSuggestionsFetch = fetch(FormatBackendURI(`labelNames.json`), { - credentials: "include" - }) + this.nameSuggestionsFetch = FetchWithCredentials( + FormatBackendURI(`labelNames.json`), + {} + ) .then( result => result.json(), err => { diff --git a/ui/src/Components/NavBar/FilterInput/index.js b/ui/src/Components/NavBar/FilterInput/index.js index 7b285880e..0dc7b5329 100644 --- a/ui/src/Components/NavBar/FilterInput/index.js +++ b/ui/src/Components/NavBar/FilterInput/index.js @@ -15,6 +15,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch"; import { AlertStore, FormatBackendURI } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { IsMobile } from "Common/Device"; +import { FetchWithCredentials } from "Common/Fetch"; import { FilterInputLabel } from "Components/Labels/FilterInputLabel"; import { AutosuggestTheme } from "./Constants"; import { History } from "./History"; @@ -72,9 +73,9 @@ const FilterInput = observer( onSuggestionsFetchRequested = debounce( action(({ value }) => { if (value !== "") { - this.inputStore.suggestionsFetch = fetch( + this.inputStore.suggestionsFetch = FetchWithCredentials( FormatBackendURI(`autocomplete.json?term=${value}`), - { credentials: "include" } + {} ) .then( result => result.json(), diff --git a/ui/src/Components/SilenceModal/AlertManagerInput/index.test.js b/ui/src/Components/SilenceModal/AlertManagerInput/index.test.js index 1313dd7ec..cd8b4b442 100644 --- a/ui/src/Components/SilenceModal/AlertManagerInput/index.test.js +++ b/ui/src/Components/SilenceModal/AlertManagerInput/index.test.js @@ -22,6 +22,7 @@ beforeEach(() => { name: "am1", uri: "http://am1.example.com", publicURI: "http://am1.example.com", + headers: {}, error: "", version: "0.15.0", cluster: "ha", @@ -31,6 +32,7 @@ beforeEach(() => { name: "am2", uri: "http://am2.example.com", publicURI: "http://am2.example.com", + headers: {}, error: "", version: "0.15.0", cluster: "ha", @@ -40,6 +42,7 @@ beforeEach(() => { name: "am3", uri: "http://am3.example.com", publicURI: "http://am3.example.com", + headers: {}, error: "", version: "0.15.0", cluster: "am3", diff --git a/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js b/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js index 4a3176af8..8fc6c6a9e 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js +++ b/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js @@ -8,6 +8,7 @@ import { SilenceFormMatcher } from "Models/SilenceForm"; import { MultiSelect } from "Components/MultiSelect"; import { ValidationError } from "Components/MultiSelect/ValidationError"; import { FormatBackendURI } from "Stores/AlertStore"; +import { FetchWithCredentials } from "Common/Fetch"; const LabelNameInput = observer( class LabelNameInput extends MultiSelect { @@ -19,9 +20,10 @@ const LabelNameInput = observer( populateNameSuggestions = action(() => { const { matcher } = this.props; - this.nameSuggestionsFetch = fetch(FormatBackendURI(`labelNames.json`), { - credentials: "include" - }) + this.nameSuggestionsFetch = FetchWithCredentials( + FormatBackendURI(`labelNames.json`), + {} + ) .then( result => result.json(), err => { @@ -43,11 +45,9 @@ const LabelNameInput = observer( populateValueSuggestions = action(() => { const { matcher } = this.props; - this.valueSuggestionsFetch = fetch( + this.valueSuggestionsFetch = FetchWithCredentials( FormatBackendURI(`labelValues.json?name=${matcher.name}`), - { - credentials: "include" - } + {} ) .then( result => result.json(), diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js index c68ef230d..8f9d19bc5 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js @@ -14,6 +14,7 @@ import { FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { SilenceFormMatcher } from "Models/SilenceForm"; import { TooltipWrapper } from "Components/TooltipWrapper"; +import { FetchWithCredentials } from "Common/Fetch"; import { MatcherToFilter, AlertManagersToFilter } from "../Matchers"; const MatchCounter = observer( @@ -54,7 +55,7 @@ const MatchCounter = observer( const alertsURI = FormatBackendURI("alerts.json?") + FormatAlertsQ(filters); - this.matchedAlerts.fetch = fetch(alertsURI, { credentials: "include" }) + this.matchedAlerts.fetch = FetchWithCredentials(alertsURI, {}) .then(result => { return result.json(); }) diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.js b/ui/src/Components/SilenceModal/SilencePreview/index.js index 41d3c8d2d..2701169e2 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.js +++ b/ui/src/Components/SilenceModal/SilencePreview/index.js @@ -15,6 +15,7 @@ import { LabelSetList, GroupListToUniqueLabelsList } from "Components/LabelSetList"; +import { FetchWithCredentials } from "Common/Fetch"; import { MatcherToFilter, AlertManagersToFilter } from "../Matchers"; const FetchError = ({ message }) => ( @@ -67,7 +68,7 @@ const SilencePreview = observer( const alertsURI = FormatBackendURI("alerts.json?") + FormatAlertsQ(filters); - this.matchedAlerts.fetch = fetch(alertsURI, { credentials: "include" }) + this.matchedAlerts.fetch = FetchWithCredentials(alertsURI, {}) .then(result => { return result.json(); }) diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js index 45c322ae7..d16ed4a6e 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js +++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js @@ -13,6 +13,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama import { APISilenceMatcher } from "Models/API"; import { AlertStore } from "Stores/AlertStore"; +import { FetchWithCredentials } from "Common/Fetch"; const SubmitState = Object.freeze({ InProgress: "InProgress", @@ -108,13 +109,13 @@ const SilenceSubmitProgress = observer( ? `${am.publicURI}/api/v2/silences` : `${am.publicURI}/api/v1/silences`; - this.submitState.fetch = fetch(uri, { + this.submitState.fetch = FetchWithCredentials(uri, { method: "POST", body: JSON.stringify(payload), headers: { - "Content-Type": "application/json" - }, - credentials: "include" + "Content-Type": "application/json", + ...am.headers + } }) .then(result => { if (isOpenAPI) { diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js index d04bab3cb..73d7852fb 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js +++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.test.js @@ -15,6 +15,7 @@ beforeEach(() => { name: "mockAlertmanager", uri: "file:///mock", publicURI: "http://example.com", + headers: { foo: "bar" }, error: "", version: "0.15.0", cluster: "mockAlertmanager", @@ -68,7 +69,7 @@ describe("", () => { const payload = fetch.mock.calls[0][1]; expect(payload).toMatchObject({ method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", foo: "bar" }, body: JSON.stringify({ matchers: [], startsAt: "now", @@ -93,6 +94,7 @@ describe("", () => { name: "am1", uri: "file:///mock", publicURI: "http://am1.example.com", + headers: {}, error: "", version: "0.15.0", cluster: "ha", @@ -102,6 +104,7 @@ describe("", () => { name: "am2", uri: "file:///mock", publicURI: "http://am2.example.com", + headers: {}, error: "", version: "0.15.0", cluster: "ha", @@ -147,6 +150,7 @@ describe("", () => { name: "am1", uri: "file:///mock", publicURI: "http://am1.example.com", + headers: {}, error: "", version: "0.15.0", cluster: "ha", diff --git a/ui/src/Models/API.js b/ui/src/Models/API.js index 46a452ef7..61710615f 100644 --- a/ui/src/Models/API.js +++ b/ui/src/Models/API.js @@ -70,6 +70,7 @@ const APIAlertmanagerUpstream = PropTypes.exact({ cluster: PropTypes.string.isRequired, uri: PropTypes.string.isRequired, publicURI: PropTypes.string.isRequired, + headers: PropTypes.object.isRequired, error: PropTypes.string.isRequired, version: PropTypes.string.isRequired, clusterMembers: PropTypes.arrayOf(PropTypes.string).isRequired diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index e6025abb5..64ecc7935 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -6,6 +6,8 @@ import equal from "fast-deep-equal"; import qs from "qs"; +import { FetchWithCredentials } from "Common/Fetch"; + const QueryStringEncodeOptions = { encodeValuesOnly: true, // don't encode q[] indices: false // go-gin doesn't support parsing q[0]=foo&q[1]=bar @@ -247,7 +249,7 @@ class AlertStore { `alerts.json?sortOrder=${sortOrder}&sortLabel=${sortLabel}&sortReverse=${sortReverse}&` ) + FormatAPIFilterQuery(this.filters.values.map(f => f.raw)); - return fetch(alertsURI, { credentials: "include" }) + return FetchWithCredentials(alertsURI, {}) .then(result => { this.status.setProcessing(); return result.json(); diff --git a/ui/src/__mocks__/Alerts.js b/ui/src/__mocks__/Alerts.js index e0059eebf..edaf24c93 100644 --- a/ui/src/__mocks__/Alerts.js +++ b/ui/src/__mocks__/Alerts.js @@ -71,6 +71,9 @@ const MockAlertmanager = () => ({ cluster: "default", uri: "http://localhost", publicURI: "http://am.example.com", + headers: { + Authorization: "Basic foo bar" + }, error: "", version: "0.15.0", clusterMembers: ["default"]