mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
Merge pull request #1468 from prymitive/proxy-rewrite-username
feat(backend): use username from credentials for silences
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user