From 4efe17e8f112c7d3d3f1fea77b2a0ec45c4073dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Thu, 26 Sep 2019 17:17:05 +0100 Subject: [PATCH] feat(api): expose alertmanager headers in the API --- cmd/karma/alerts.go | 10 +++ internal/alertmanager/model_test.go | 4 +- internal/alertmanager/models.go | 2 +- internal/models/alertmanager.go | 11 +-- internal/uri/urls.go | 33 +++++++++ internal/uri/urls_test.go | 102 ++++++++++++++++++++++++---- 6 files changed, 141 insertions(+), 21 deletions(-) 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/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) + } + } +}