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 17:18:27 +01:00
parent 4efe17e8f1
commit 80c30f1879
17 changed files with 121 additions and 25 deletions

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
ui/src/Common/Fetch.js Normal file
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 };

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"
});
});
});

View File

@@ -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) {

View File

@@ -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(

View File

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

View File

@@ -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 => {

View File

@@ -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(),

View File

@@ -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",

View File

@@ -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(),

View File

@@ -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();
})

View File

@@ -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();
})

View File

@@ -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) {

View File

@@ -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",

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

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();

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