feat(backend): use username from credentials for silences

When authentication is enabled and proxy mode is on then ensure all silences are created with the username from credentials, rather than what the user sends.
This commit is contained in:
Łukasz Mierzwa
2020-02-26 11:26:19 +00:00
parent 442c20e12a
commit 2ab6892c85
6 changed files with 315 additions and 21 deletions
+13 -12
View File
@@ -47,6 +47,8 @@ var (
staticBuildFileSystem = newBinaryFileSystem("ui/build")
staticSrcFileSystem = newBinaryFileSystem("ui/src")
protectedEndpoints *gin.RouterGroup
)
func getViewURL(sub string) string {
@@ -117,10 +119,9 @@ func setupRouter(router *gin.Engine) {
ExposeHeaders: []string{"Content-Length"},
}))
var protected *gin.RouterGroup
if config.Config.Authentication.Header.Name != "" {
config.Config.Authentication.Enabled = true
protected = router.Group(getViewURL("/"),
protectedEndpoints = router.Group(getViewURL("/"),
headerAuth(config.Config.Authentication.Header.Name, config.Config.Authentication.Header.ValueRegex))
} else if len(config.Config.Authentication.BasicAuth.Users) > 0 {
config.Config.Authentication.Enabled = true
@@ -128,22 +129,22 @@ func setupRouter(router *gin.Engine) {
for _, u := range config.Config.Authentication.BasicAuth.Users {
users[u.Username] = u.Password
}
protected = router.Group(getViewURL("/"), gin.BasicAuth(users))
protectedEndpoints = router.Group(getViewURL("/"), gin.BasicAuth(users))
} else {
protected = router.Group(getViewURL("/"))
protectedEndpoints = router.Group(getViewURL("/"))
}
router.GET(getViewURL("/health"), pong)
protected.GET("/", index)
protected.GET("/alerts.json", alerts)
protected.GET("/autocomplete.json", autocomplete)
protected.GET("/labelNames.json", knownLabelNames)
protected.GET("/labelValues.json", knownLabelValues)
protected.GET("/silences.json", silences)
protectedEndpoints.GET("/", index)
protectedEndpoints.GET("/alerts.json", alerts)
protectedEndpoints.GET("/autocomplete.json", autocomplete)
protectedEndpoints.GET("/labelNames.json", knownLabelNames)
protectedEndpoints.GET("/labelValues.json", knownLabelValues)
protectedEndpoints.GET("/silences.json", silences)
protected.GET("/custom.css", customCSS)
protected.GET("/custom.js", customJS)
protectedEndpoints.GET("/custom.css", customCSS)
protectedEndpoints.GET("/custom.js", customJS)
router.NoRoute(notFound)
}
+50 -5
View File
@@ -1,7 +1,9 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
@@ -10,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/config"
"github.com/prymitive/karma/internal/mapper"
log "github.com/sirupsen/logrus"
)
@@ -23,7 +26,7 @@ func proxyPathPrefix(name string) string {
}
func proxyPath(name, path string) string {
return fmt.Sprintf("%s%s", proxyPathPrefix(name), path)
return fmt.Sprintf("/proxy/alertmanager/%s%s", name, path)
}
// NewAlertmanagerProxy creates a proxy instance for given alertmanager instance
@@ -56,7 +59,7 @@ func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.Re
req.URL.Path = strings.TrimSuffix(upstreamURL.Path, "/") + req.URL.Path
}
log.Debugf("[%s] Proxy request for %s", alertmanager.Name, req.URL.Path)
log.Debugf("[%s] Forwarding request for %s to %s", alertmanager.Name, req.RequestURI, req.URL.String())
},
Transport: alertmanager.HTTPTransport,
ModifyResponse: func(resp *http.Response) error {
@@ -69,15 +72,57 @@ func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.Re
return &proxy, nil
}
func handlePostRequest(alertmanager *alertmanager.Alertmanager, h http.Handler) gin.HandlerFunc {
return func(c *gin.Context) {
log.Debugf("[%s] Proxy request %s", alertmanager.Name, c.Request.RequestURI)
if config.Config.Authentication.Enabled {
body, err := ioutil.ReadAll(c.Request.Body)
c.Request.Body.Close()
if err != nil {
log.Errorf("[%s] proxy request '%s %s' body close failed: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
ver := alertmanager.Version()
if ver == "" {
ver = "999.0"
}
username := c.MustGet(gin.AuthUserKey).(string)
m, err := mapper.GetSilenceMapper(ver)
if err != nil {
log.Errorf("[%s] proxy request '%s %s' error: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
newBody, err := m.RewriteUsername(body, username)
if err != nil {
log.Errorf("[%s] proxy request '%s %s' silence body rewrite error: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(newBody))
c.Request.ContentLength = int64(len(newBody))
c.Request.Header.Set("Content-Length", fmt.Sprintf("%d", c.Request.ContentLength))
}
h.ServeHTTP(c.Writer, c.Request)
}
}
func setupRouterProxyHandlers(router *gin.Engine, alertmanager *alertmanager.Alertmanager) error {
proxy, err := NewAlertmanagerProxy(alertmanager)
if err != nil {
return err
}
router.POST(
protectedEndpoints.POST(
proxyPath(alertmanager.Name, "/api/v2/silences"),
gin.WrapH(http.StripPrefix(proxyPathPrefix(alertmanager.Name), proxy)))
router.DELETE(
handlePostRequest(alertmanager, http.StripPrefix(proxyPathPrefix(alertmanager.Name), proxy)))
protectedEndpoints.DELETE(
proxyPath(alertmanager.Name, "/api/v2/silence/*id"),
gin.WrapH(http.StripPrefix(proxyPathPrefix(alertmanager.Name), proxy)))
return nil
+236 -4
View File
@@ -1,15 +1,22 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
cache "github.com/patrickmn/go-cache"
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/config"
"github.com/prymitive/karma/internal/mock"
log "github.com/sirupsen/logrus"
"github.com/jarcoal/httpmock"
"github.com/pmezard/go-difflib/difflib"
)
// httptest.NewRecorder() doesn't implement http.CloseNotifier
@@ -271,11 +278,10 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
}
for _, testCase := range proxyTests {
t.Run(testCase.alertmanagerURI, func(t *testing.T) {
t.Run(fmt.Sprintf("prefix=%s|uri=%s", testCase.listenPrefix, testCase.alertmanagerURI), func(t *testing.T) {
httpmock.Reset()
r := ginTestEngine()
config.Config.Listen.Prefix = testCase.listenPrefix
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"suburi",
@@ -299,7 +305,233 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
resp := newCloseNotifyingRecorder()
r.ServeHTTP(resp, req)
if resp.Code != 200 {
t.Errorf("Got response code %d instead of 200", resp.Code)
t.Errorf("Got response code %d instead of 200 for %s", resp.Code, testCase.requestURI)
}
})
}
}
func TestProxyUserRewrite(t *testing.T) {
type proxyTest struct {
name string
headerName string
headerRe string
basicAuthUsers []config.AuthenticationUser
requestHeaders map[string]string
requestBasicAuthUser string
requestBasicAuthPassword string
frontednRequestBody string
proxyRequestBody string
responseCode int
}
proxyTests := []proxyTest{
{
name: "no-auth, no-op",
responseCode: 200,
frontednRequestBody: `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`,
proxyRequestBody: `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`,
},
{
name: "basicAuth, correct credentials, invalid JSON",
responseCode: 500,
basicAuthUsers: []config.AuthenticationUser{
{Username: "john", Password: "foobar"},
},
requestBasicAuthUser: "john",
requestBasicAuthPassword: "foobar",
frontednRequestBody: `{XXX`,
},
{
name: "basicAuth, missing credentials",
responseCode: 401,
basicAuthUsers: []config.AuthenticationUser{
{Username: "john", Password: "foobar"},
},
},
{
name: "basicAuth, correct credentials, fixed username",
responseCode: 200,
basicAuthUsers: []config.AuthenticationUser{
{Username: "john", Password: "foobar"},
},
requestBasicAuthUser: "john",
requestBasicAuthPassword: "foobar",
frontednRequestBody: `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`,
proxyRequestBody: `{"comment":"comment","createdBy":"john","endsAt":"2000-02-01T00:02:03.000Z","matchers":[{"isRegex":false,"name":"alertname","value":"Fake Alert"},{"isRegex":true,"name":"foo","value":"(bar|baz)"}],"startsAt":"2000-02-01T00:00:00.000Z"}`,
},
{
name: "basicAuth, correct credentials, fixed username, silence ID",
responseCode: 200,
basicAuthUsers: []config.AuthenticationUser{
{Username: "john", Password: "foobar"},
},
requestBasicAuthUser: "john",
requestBasicAuthPassword: "foobar",
frontednRequestBody: `{
"id": "1234567890",
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`,
proxyRequestBody: `{"id":"1234567890","comment":"comment","createdBy":"john","endsAt":"2000-02-01T00:02:03.000Z","matchers":[{"isRegex":false,"name":"alertname","value":"Fake Alert"},{"isRegex":true,"name":"foo","value":"(bar|baz)"}],"startsAt":"2000-02-01T00:00:00.000Z"}`,
},
{
name: "header auth, missing header",
responseCode: 401,
headerName: "X-Auth",
headerRe: "(.+)",
},
{
name: "header auth, invalid header",
headerName: "X-Auth",
headerRe: "Username (.+)",
requestHeaders: map[string]string{
"X-Auth": "xxx",
},
responseCode: 401,
},
{
name: "header auth, correct credentials, fixed username",
responseCode: 200,
headerName: "X-Auth",
headerRe: "(.+)",
requestHeaders: map[string]string{
"X-Auth": "john",
},
frontednRequestBody: `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`,
proxyRequestBody: `{"comment":"comment","createdBy":"john","endsAt":"2000-02-01T00:02:03.000Z","matchers":[{"isRegex":false,"name":"alertname","value":"Fake Alert"},{"isRegex":true,"name":"foo","value":"(bar|baz)"}],"startsAt":"2000-02-01T00:00:00.000Z"}`,
},
{
name: "basicAuth, correct credentials, fixed username, silence ID",
responseCode: 200,
headerName: "X-Auth",
headerRe: "Username (.+)",
requestHeaders: map[string]string{
"X-Auth": "Username john",
},
frontednRequestBody: `{
"id": "1234567890",
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`,
proxyRequestBody: `{"id":"1234567890","comment":"comment","createdBy":"john","endsAt":"2000-02-01T00:02:03.000Z","matchers":[{"isRegex":false,"name":"alertname","value":"Fake Alert"},{"isRegex":true,"name":"foo","value":"(bar|baz)"}],"startsAt":"2000-02-01T00:00:00.000Z"}`,
},
}
for _, testCase := range proxyTests {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
log.SetLevel(log.FatalLevel)
t.Run(testCase.name, func(t *testing.T) {
for _, version := range mock.ListAllMocks() {
t.Logf("Testing alerts using mock files from Alertmanager %s", version)
config.Config.Listen.Prefix = "/"
config.Config.Authentication.Header.Name = testCase.headerName
config.Config.Authentication.Header.ValueRegex = testCase.headerRe
config.Config.Authentication.BasicAuth.Users = testCase.basicAuthUsers
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"proxyAuth",
"http://localhost",
alertmanager.WithRequestTimeout(time.Second*5),
alertmanager.WithProxy(true),
)
if err != nil {
t.Error(err)
}
err = setupRouterProxyHandlers(r, am)
if err != nil {
t.Errorf("Failed to setup proxy for Alertmanager %s: %s", am.Name, err)
}
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
httpmock.Reset()
mock.RegisterURL("http://localhost/metrics", version, "metrics")
mock.RegisterURL("http://localhost/api/v2/status", version, "api/v2/status")
mock.RegisterURL("http://localhost/api/v2/silences", version, "api/v2/silences")
mock.RegisterURL("http://localhost/api/v2/alerts/groups", version, "api/v2/alerts/groups")
_ = am.Pull()
httpmock.RegisterResponder("POST", "http://localhost/api/v2/silences", func(req *http.Request) (*http.Response, error) {
body, _ := ioutil.ReadAll(req.Body)
return httpmock.NewBytesResponse(200, body), nil
})
req := httptest.NewRequest("POST", "/proxy/alertmanager/proxyAuth/api/v2/silences", ioutil.NopCloser(bytes.NewBufferString(testCase.frontednRequestBody)))
for k, v := range testCase.requestHeaders {
req.Header.Set(k, v)
}
req.SetBasicAuth(testCase.requestBasicAuthUser, testCase.requestBasicAuthPassword)
resp := newCloseNotifyingRecorder()
r.ServeHTTP(resp, req)
if resp.Code != testCase.responseCode {
t.Errorf("Got response code %d instead of %d", resp.Code, testCase.responseCode)
}
gotBody, _ := ioutil.ReadAll(resp.Body)
if string(gotBody) != testCase.proxyRequestBody {
diff := difflib.UnifiedDiff{
A: difflib.SplitLines(testCase.proxyRequestBody),
B: difflib.SplitLines(string(gotBody)),
FromFile: "Expected",
ToFile: "Response",
Context: 3,
}
text, err := difflib.GetUnifiedDiffString(diff)
if err != nil {
t.Error(err)
}
t.Errorf("Body mismatch:\n%s", text)
}
}
})
}
+1
View File
@@ -30,6 +30,7 @@ type AlertMapper interface {
type SilenceMapper interface {
Mapper
Collect(string, map[string]string, time.Duration, http.RoundTripper) ([]models.Silence, error)
RewriteUsername([]byte, string) ([]byte, error)
}
// StatusMapper handles mapping Alertmanager status information containing cluster config
+11
View File
@@ -14,6 +14,7 @@ import (
"github.com/prymitive/karma/internal/mapper/v017/client/alertgroup"
"github.com/prymitive/karma/internal/mapper/v017/client/general"
"github.com/prymitive/karma/internal/mapper/v017/client/silence"
ammodels "github.com/prymitive/karma/internal/mapper/v017/models"
"github.com/prymitive/karma/internal/models"
)
@@ -119,3 +120,13 @@ func status(c *client.Alertmanager, timeout time.Duration) (models.AlertmanagerS
return ret, nil
}
func rewriteSilenceUsername(body []byte, username string) ([]byte, error) {
s := ammodels.PostableSilence{}
err := s.UnmarshalBinary(body)
if err != nil {
return nil, err
}
s.CreatedBy = &username
return s.MarshalBinary()
}
+4
View File
@@ -27,3 +27,7 @@ func (m SilenceMapper) Collect(uri string, headers map[string]string, timeout ti
c := newClient(uri, headers, httpTransport)
return silences(c, timeout)
}
func (m SilenceMapper) RewriteUsername(body []byte, username string) ([]byte, error) {
return rewriteSilenceUsername(body, username)
}