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"]