diff --git a/cmd/karma/alerts.go b/cmd/karma/alerts.go index 7e3f27482..7efec171f 100644 --- a/cmd/karma/alerts.go +++ b/cmd/karma/alerts.go @@ -13,6 +13,7 @@ import ( "github.com/prymitive/karma/internal/filters" "github.com/prymitive/karma/internal/models" "github.com/prymitive/karma/internal/slices" + "github.com/prymitive/karma/internal/uri" log "github.com/sirupsen/logrus" ) @@ -111,11 +112,20 @@ func getUpstreams() models.AlertmanagerAPISummary { Name: upstream.Name, URI: upstream.SanitizedURI(), PublicURI: upstream.PublicURI(), + Headers: map[string]string{}, Error: upstream.Error(), Version: upstream.Version(), Cluster: upstream.ClusterID(), ClusterMembers: members, } + if !upstream.ProxyRequests { + for k, v := range uri.HeadersForBasicAuth(u.PublicURI) { + u.Headers[k] = v + } + for k, v := range upstream.HTTPHeaders { + u.Headers[k] = v + } + } summary.Instances = append(summary.Instances, u) summary.Counters.Total++ diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3e0844f8f..562e0b98c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -69,10 +69,10 @@ alertmanager: (`https://user:password@alertmanager.example.com`) and you don't want it to be visible to users then ensure `proxy: true` is also set in order to avoid leaking auth information to the browser. - Without proxy mode full URI needs to be passed to karma web UI code. - With proxy mode all requests will be routed via karma HTTP server and since - karma has full URI in the config it only needs Alertmanager name in that - request. + Note: if URI contains username and password and proxy option is NOT enabled + (see below), then the username & password information will be stripped from + the URI and `Authorization` header using Basic Auth will be set for all + in browser requests. To set a different URI for all browser requests (can be any valid URI) see `external_uri` option below. - `external_uri` - base URI of this Alertmanager server used for all browser diff --git a/internal/alertmanager/model_test.go b/internal/alertmanager/model_test.go index 3adfce608..89a6e879b 100644 --- a/internal/alertmanager/model_test.go +++ b/internal/alertmanager/model_test.go @@ -35,12 +35,12 @@ var uriTests = []uriTest{ { rawURI: "http://user:pass@alertmanager.example.com", proxy: false, - publicURI: "http://user:pass@alertmanager.example.com", + publicURI: "http://alertmanager.example.com", }, { rawURI: "https://user:pass@alertmanager.example.com/foo", proxy: false, - publicURI: "https://user:pass@alertmanager.example.com/foo", + publicURI: "https://alertmanager.example.com/foo", }, { rawURI: "http://user:pass@alertmanager.example.com", diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 551dda3eb..9a4dcd6fb 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -211,7 +211,7 @@ func (am *Alertmanager) PublicURI() string { if am.ExternalURI != "" { return am.ExternalURI } - return am.URI + return uri.WithoutUserinfo(am.URI) } func (am *Alertmanager) pullAlerts(version string) error { diff --git a/internal/models/alertmanager.go b/internal/models/alertmanager.go index 930224716..2eb08980d 100644 --- a/internal/models/alertmanager.go +++ b/internal/models/alertmanager.go @@ -29,11 +29,12 @@ type AlertmanagerAPIStatus struct { URI string `json:"uri"` // this is URI client should use to talk to this Alertmanager, it might be // same as real or proxied URI - PublicURI string `json:"publicURI"` - Error string `json:"error"` - Version string `json:"version"` - Cluster string `json:"cluster"` - ClusterMembers []string `json:"clusterMembers"` + PublicURI string `json:"publicURI"` + Headers map[string]string `json:"headers"` + Error string `json:"error"` + Version string `json:"version"` + Cluster string `json:"cluster"` + ClusterMembers []string `json:"clusterMembers"` } // AlertmanagerAPICounters returns number of Alertmanager instances in each diff --git a/internal/uri/urls.go b/internal/uri/urls.go index 07c0e8035..13e77072d 100644 --- a/internal/uri/urls.go +++ b/internal/uri/urls.go @@ -1,6 +1,7 @@ package uri import ( + "encoding/base64" "net/url" "path" ) @@ -30,3 +31,35 @@ func SanitizeURI(s string) string { } return s } + +// HeadersForBasicAuth checks if the passed uri contains user & password +// (http://user:pass@example.com) and if so generates headers for Basic Auth +// based on +func HeadersForBasicAuth(s string) map[string]string { + headers := map[string]string{} + + u, err := url.Parse(s) + if err != nil { + return headers + } + + if u.User != nil { + if password, pwdSet := u.User.Password(); pwdSet { + auth := u.User.Username() + ":" + password + headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + } + } + + return headers +} + +// WithoutUserinfo takes an URL and returns a copy of it with basic auth +// stripped +func WithoutUserinfo(s string) string { + u, err := url.Parse(s) + if err != nil { + return s + } + u.User = nil + return u.String() +} diff --git a/internal/uri/urls_test.go b/internal/uri/urls_test.go index d0e6d7337..998e2a735 100644 --- a/internal/uri/urls_test.go +++ b/internal/uri/urls_test.go @@ -7,35 +7,48 @@ import ( ) type joinURLTest struct { - base string - sub string - url string + base string + sub string + url string + isValid bool } var joinURLTests = []joinURLTest{ { - base: "http://localhost", - sub: "/sub", - url: "http://localhost/sub", + base: "http://localhost", + sub: "/sub", + url: "http://localhost/sub", + isValid: true, }, { - base: "http://localhost", - sub: "/sub/", - url: "http://localhost/sub", + base: "http://localhost", + sub: "/sub/", + url: "http://localhost/sub", + isValid: true, }, { - base: "http://am.example.com", - sub: "/api/v1/alerts", - url: "http://am.example.com/api/v1/alerts", + base: "http://am.example.com", + sub: "/api/v1/alerts", + url: "http://am.example.com/api/v1/alerts", + isValid: true, + }, + { + base: "%gh&%ij", + sub: "/a + b", + url: "", + isValid: false, }, } func TestJoinURL(t *testing.T) { for _, testCase := range joinURLTests { url, err := uri.JoinURL(testCase.base, testCase.sub) - if err != nil { + if err != nil && testCase.isValid { t.Errorf("joinURL(%v, %v) failed: %s", testCase.base, testCase.sub, err.Error()) } + if err == nil && !testCase.isValid { + t.Errorf("expected error for '%s' and '%s' but got '%s'", testCase.base, testCase.sub, url) + } if url != testCase.url { t.Errorf("Invalid joined url from '%s' + '%s', expected '%s', got '%s'", testCase.base, testCase.sub, testCase.url, url) } @@ -80,6 +93,10 @@ var sanitizeURITests = []sanitizeURITest{ raw: "https://user:pass@alertmanager.example.com/foo", sanitized: "https://user:xxx@alertmanager.example.com/foo", }, + { + raw: "%gh&%ij", + sanitized: "%gh&%ij", + }, } func TestSanitizedURI(t *testing.T) { @@ -91,3 +108,62 @@ func TestSanitizedURI(t *testing.T) { } } } + +func TestHeadersForBasicAuth(t *testing.T) { + type headersTest struct { + uri string + isSet bool + value string + } + testCases := []headersTest{ + { + uri: "http://localhost.com", + isSet: false, + }, + { + uri: "http://user@localhost.com", + isSet: false, + }, + { + uri: "http://user:pass@localhost.com", + isSet: true, + value: "Basic dXNlcjpwYXNz", + }, + { + uri: "%gh&%ij", + isSet: false, + }, + } + for _, test := range testCases { + headers := uri.HeadersForBasicAuth(test.uri) + value, isSet := headers["Authorization"] + if isSet != test.isSet { + t.Errorf("[%s] expected Authorization header: %v, was set: %v", test.uri, test.isSet, isSet) + } + if value != test.value { + t.Errorf("[%s] expected Authorization value: %s, value: %s", test.uri, test.value, value) + } + } +} + +func TestURIWithoutUserinfo(t *testing.T) { + type userinfoTest struct { + uri string + parsed string + } + testCases := []userinfoTest{ + {uri: "http://localhost", parsed: "http://localhost"}, + {uri: "http://localhost?foo=bar", parsed: "http://localhost?foo=bar"}, + {uri: "http://user@localhost", parsed: "http://localhost"}, + {uri: "http://user:pass@localhost", parsed: "http://localhost"}, + {uri: "http://user:pass@localhost?foo=bar#1", parsed: "http://localhost?foo=bar#1"}, + {uri: "%gh&%ij", parsed: "%gh&%ij"}, + } + + for _, test := range testCases { + parsed := uri.WithoutUserinfo(test.uri) + if parsed != test.parsed { + t.Errorf("'%s' got parsed as '%s', expected: '%s'", test.uri, parsed, test.parsed) + } + } +} 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"]