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.
This commit is contained in:
Łukasz Mierzwa
2019-09-26 20:37:23 +01:00
parent 4efe17e8f1
commit 80c30f1879
17 changed files with 121 additions and 25 deletions
+1
View File
@@ -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",
+6
View File
@@ -0,0 +1,6 @@
import merge from "lodash.merge";
const FetchWithCredentials = async (uri, options) =>
await fetch(uri, merge({}, { credentials: "include" }, options));
export { FetchWithCredentials };
+40
View File
@@ -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"
});
});
});
@@ -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) {
@@ -122,7 +122,7 @@ describe("<DeleteSilenceModalContent />", () => {
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("<DeleteSilenceModalContent />", () => {
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("<DeleteSilenceModalContent />", () => {
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(
@@ -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("<Silence />", () => {
cluster: "default",
uri: "file:///mock",
publicURI: "http://example.com",
headers: {},
error: "",
version: "0.15.0",
clusterMembers: ["default"]
@@ -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 => {
@@ -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(),
@@ -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",
@@ -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(),
@@ -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();
})
@@ -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();
})
@@ -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) {
@@ -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("<SilenceSubmitProgress />", () => {
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("<SilenceSubmitProgress />", () => {
name: "am1",
uri: "file:///mock",
publicURI: "http://am1.example.com",
headers: {},
error: "",
version: "0.15.0",
cluster: "ha",
@@ -102,6 +104,7 @@ describe("<SilenceSubmitProgress />", () => {
name: "am2",
uri: "file:///mock",
publicURI: "http://am2.example.com",
headers: {},
error: "",
version: "0.15.0",
cluster: "ha",
@@ -147,6 +150,7 @@ describe("<SilenceSubmitProgress />", () => {
name: "am1",
uri: "file:///mock",
publicURI: "http://am1.example.com",
headers: {},
error: "",
version: "0.15.0",
cluster: "ha",
+1
View File
@@ -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
+3 -1
View File
@@ -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();
+3
View File
@@ -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"]