mirror of
https://github.com/prymitive/karma
synced 2026-08-23 11:56:20 +00:00
Merge pull request #1496 from prymitive/silence-acl
feat(backend): add configuration options for silence ACL rules
This commit is contained in:
@@ -10,6 +10,9 @@ requires Alertmanager `>=0.17.0`.
|
||||
|
||||
---
|
||||
|
||||
See [GitHub Releases](https://github.com/prymitive/karma/releases) for release
|
||||
changelog.
|
||||
|
||||
## Feature overview
|
||||
|
||||
Alertmanager UI is useful for browsing alerts and managing silences, but it's
|
||||
@@ -50,6 +53,8 @@ screen space, the silence will also be moved to the footer.
|
||||
|
||||
Silence modal allows to create new silences and manage all silences already
|
||||
present in Alertmanager.
|
||||
Silence ACL rules can be used to control silence creation and editing, see
|
||||
[ACLs](/docs/ACLs.md) docs for more details.
|
||||
|
||||

|
||||
|
||||
@@ -140,6 +145,9 @@ to modify data in Alertmanager instance, then please ensure that:
|
||||
for all alertmanager instances, this options will disable any UI elements that
|
||||
could trigger updates (like silence management)
|
||||
|
||||
To restrict some users from creating silences or enforce some matcher rules use
|
||||
[silence ACL rules](/docs/ACLs.md). This feature requires `proxy` to be enabled.
|
||||
|
||||
## Metrics
|
||||
|
||||
karma process metrics are accessible under `/metrics` path by default.
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/prymitive/karma/internal/alertmanager"
|
||||
"github.com/prymitive/karma/internal/config"
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
"github.com/prymitive/karma/internal/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
aclActionRequireMatcher = "requireMatcher"
|
||||
aclActionBlock = "block"
|
||||
aclActionAllow = "allow"
|
||||
)
|
||||
|
||||
var (
|
||||
allACLActions = []string{aclActionAllow, aclActionBlock, aclActionRequireMatcher}
|
||||
)
|
||||
|
||||
type silenceFilter struct {
|
||||
Name string
|
||||
NameRegex *regexp.Regexp
|
||||
Value string
|
||||
ValueRegex *regexp.Regexp
|
||||
IsRegex bool
|
||||
}
|
||||
|
||||
func (sf *silenceFilter) isMatch(silence *models.Silence) bool {
|
||||
for _, m := range silence.Matchers {
|
||||
var nameMatch bool
|
||||
if sf.Name != "" && sf.Name == m.Name {
|
||||
nameMatch = true
|
||||
} else if sf.NameRegex != nil && sf.NameRegex.MatchString(m.Name) {
|
||||
nameMatch = true
|
||||
}
|
||||
|
||||
var valueMatch bool
|
||||
if sf.Value != "" && sf.Value == m.Value {
|
||||
valueMatch = true
|
||||
} else if sf.ValueRegex != nil && sf.ValueRegex.MatchString(m.Value) {
|
||||
valueMatch = true
|
||||
}
|
||||
|
||||
if nameMatch && valueMatch && sf.IsRegex == m.IsRegex {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type silenceMatcher struct {
|
||||
Name string
|
||||
Value string
|
||||
IsRegex bool
|
||||
}
|
||||
|
||||
type aclMatchers struct {
|
||||
Required []silenceMatcher
|
||||
}
|
||||
|
||||
type silenceACLScope struct {
|
||||
Groups []string
|
||||
Alertmanagers []string
|
||||
Filters []silenceFilter
|
||||
}
|
||||
|
||||
type silenceACL struct {
|
||||
Action string
|
||||
Reason string
|
||||
Scope silenceACLScope
|
||||
Matchers aclMatchers
|
||||
}
|
||||
|
||||
func (acl *silenceACL) isAllowed(amName string, silence *models.Silence, username string) (bool, error) {
|
||||
groups := userGroups(username)
|
||||
|
||||
groupMatch := len(acl.Scope.Groups) == 0
|
||||
for _, aclGroup := range acl.Scope.Groups {
|
||||
if slices.StringInSlice(groups, aclGroup) {
|
||||
groupMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
amMatch := len(acl.Scope.Alertmanagers) == 0
|
||||
for _, aclAM := range acl.Scope.Alertmanagers {
|
||||
if amName == aclAM {
|
||||
amMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
filterMatch := len(acl.Scope.Filters) == 0
|
||||
for _, aclFilter := range acl.Scope.Filters {
|
||||
filterMatch = aclFilter.isMatch(silence)
|
||||
}
|
||||
|
||||
if groupMatch && amMatch && filterMatch {
|
||||
switch acl.Action {
|
||||
case aclActionAllow:
|
||||
return true, nil
|
||||
case aclActionBlock:
|
||||
return false, fmt.Errorf("silence blocked by ACL rule: %s", acl.Reason)
|
||||
case aclActionRequireMatcher:
|
||||
for _, aclM := range acl.Matchers.Required {
|
||||
var wasFound bool
|
||||
for _, m := range silence.Matchers {
|
||||
if m.Name == aclM.Name && m.Value == aclM.Value && m.IsRegex == aclM.IsRegex {
|
||||
wasFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !wasFound {
|
||||
return false, fmt.Errorf("silence blocked by ACL rule: %s", acl.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func newSilenceACLFromConfig(cfg config.SilenceACLRule) (*silenceACL, error) {
|
||||
acl := silenceACL{
|
||||
Action: cfg.Action,
|
||||
Reason: cfg.Reason,
|
||||
Scope: silenceACLScope{
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{},
|
||||
Filters: []silenceFilter{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
}
|
||||
|
||||
if !slices.StringInSlice(allACLActions, acl.Action) {
|
||||
return nil, fmt.Errorf("silence ACL rule requires 'action' to be one of %v, got %q", allACLActions, acl.Action)
|
||||
}
|
||||
|
||||
if acl.Reason == "" {
|
||||
return nil, fmt.Errorf("silence ACL rule requires 'reason' to be set")
|
||||
}
|
||||
|
||||
for _, groupName := range cfg.Scope.Groups {
|
||||
var wasFound bool
|
||||
for _, authGroup := range config.Config.Authorization.Groups {
|
||||
if authGroup.Name == groupName {
|
||||
wasFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !wasFound {
|
||||
return nil, fmt.Errorf("invalid silence ACL rule, no group with name %q found in authorization.groups configuration", groupName)
|
||||
}
|
||||
acl.Scope.Groups = append(acl.Scope.Groups, groupName)
|
||||
}
|
||||
|
||||
for _, amName := range cfg.Scope.Alertmanagers {
|
||||
am := alertmanager.GetAlertmanagerByName(amName)
|
||||
if am == nil {
|
||||
return nil, fmt.Errorf("invalid ACL rule, no alertmanager with name %q found", amName)
|
||||
}
|
||||
acl.Scope.Alertmanagers = append(acl.Scope.Alertmanagers, am.Name)
|
||||
}
|
||||
|
||||
for _, filter := range cfg.Scope.Filters {
|
||||
if filter.Name == "" && filter.NameRegex == "" {
|
||||
return nil, fmt.Errorf("silence ACL rule filter requires 'name' or 'name_re' to be set")
|
||||
}
|
||||
if filter.Name != "" && filter.NameRegex != "" {
|
||||
return nil, fmt.Errorf("silence ACL rule filter can only have 'name' or 'name_re' set, not both")
|
||||
}
|
||||
|
||||
if filter.Value == "" && filter.ValueRegex == "" {
|
||||
return nil, fmt.Errorf("silence ACL rule filter requires 'value' or 'value_re' to be set")
|
||||
}
|
||||
if filter.Value != "" && filter.ValueRegex != "" {
|
||||
return nil, fmt.Errorf("silence ACL rule filter can only have 'value' or 'value_re' set, not both")
|
||||
}
|
||||
|
||||
f := silenceFilter{
|
||||
Name: filter.Name,
|
||||
Value: filter.Value,
|
||||
IsRegex: filter.IsRegex,
|
||||
}
|
||||
|
||||
if filter.NameRegex != "" {
|
||||
re, err := regexp.Compile(filter.NameRegex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ACL rule, failed to parse name_re %q: %s", filter.NameRegex, err)
|
||||
}
|
||||
f.NameRegex = re
|
||||
}
|
||||
|
||||
if filter.ValueRegex != "" {
|
||||
re, err := regexp.Compile(filter.ValueRegex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ACL rule, failed to parse value_re %q: %s", filter.ValueRegex, err)
|
||||
}
|
||||
f.ValueRegex = re
|
||||
}
|
||||
|
||||
acl.Scope.Filters = append(acl.Scope.Filters, f)
|
||||
}
|
||||
|
||||
if acl.Action == aclActionRequireMatcher {
|
||||
for _, matcherConfig := range cfg.Matchers.Required {
|
||||
if matcherConfig.Name == "" {
|
||||
return nil, fmt.Errorf("silence ACL rule matcher requires 'name' to be set")
|
||||
}
|
||||
|
||||
if matcherConfig.Value == "" {
|
||||
return nil, fmt.Errorf("silence ACL rule matcher requires 'value' to be set")
|
||||
}
|
||||
|
||||
m := silenceMatcher{
|
||||
Name: matcherConfig.Name,
|
||||
Value: matcherConfig.Value,
|
||||
IsRegex: matcherConfig.IsRegex,
|
||||
}
|
||||
|
||||
acl.Matchers.Required = append(acl.Matchers.Required, m)
|
||||
}
|
||||
}
|
||||
|
||||
return &acl, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/prymitive/karma/internal/config"
|
||||
"github.com/prymitive/karma/internal/slices"
|
||||
)
|
||||
|
||||
func userGroups(username string) []string {
|
||||
groups := []string{}
|
||||
for _, authGroup := range config.Config.Authorization.Groups {
|
||||
if slices.StringInSlice(authGroup.Members, username) {
|
||||
groups = append(groups, authGroup.Name)
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
@@ -50,6 +50,8 @@ var (
|
||||
staticSrcFileSystem = newBinaryFileSystem("ui/src")
|
||||
|
||||
protectedEndpoints *gin.RouterGroup
|
||||
|
||||
silenceACLs = []*silenceACL{}
|
||||
)
|
||||
|
||||
func getViewURL(sub string) string {
|
||||
@@ -291,6 +293,23 @@ func mainSetup(errorHandling pflag.ErrorHandling) (*gin.Engine, error) {
|
||||
return nil, fmt.Errorf("No valid Alertmanager URIs defined")
|
||||
}
|
||||
|
||||
if config.Config.Authorization.ACL.Silences != "" {
|
||||
log.Infof("Reading silence ACL config file %s", config.Config.Authorization.ACL.Silences)
|
||||
aclConfig, err := config.ReadSilenceACLConfig(config.Config.Authorization.ACL.Silences)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, cfg := range aclConfig.Rules {
|
||||
acl, err := newSilenceACLFromConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid silence ACL rule at position %d: %s", i, err)
|
||||
}
|
||||
silenceACLs = append(silenceACLs, acl)
|
||||
}
|
||||
log.Infof("Parsed %d ACL rule(s)", len(silenceACLs))
|
||||
}
|
||||
|
||||
if *validateConfig {
|
||||
log.Info("Configuration is valid")
|
||||
return nil, nil
|
||||
|
||||
+45
-21
@@ -75,38 +75,62 @@ func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.Re
|
||||
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)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
silence, err := m.Unmarshal(body)
|
||||
if err != nil {
|
||||
log.Errorf("[%s] proxy request '%s %s' failed to unmarshal silence body: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for i, acl := range silenceACLs {
|
||||
username := c.GetString(gin.AuthUserKey)
|
||||
isAllowed, err := acl.isAllowed(alertmanager.Name, silence, username)
|
||||
log.Debugf("ACL %d: isAllowed=%v err=%v", i, isAllowed, err)
|
||||
if err != nil {
|
||||
log.Warningf("[%s] proxy request '%s %s' was blocked by ACL rule: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if isAllowed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
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))
|
||||
} else {
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
h.ServeHTTP(c.Writer, c.Request)
|
||||
|
||||
+502
-3
@@ -6,6 +6,8 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -93,6 +95,14 @@ var proxyTests = []proxyTest{
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
dummySilence := `{
|
||||
"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" }]
|
||||
}`
|
||||
|
||||
r := ginTestEngine()
|
||||
am, err := alertmanager.NewAlertmanager(
|
||||
"dummy",
|
||||
@@ -116,7 +126,7 @@ func TestProxy(t *testing.T) {
|
||||
if testCase.upstreamURI != "" {
|
||||
httpmock.RegisterResponder(testCase.method, testCase.upstreamURI, httpmock.NewStringResponder(testCase.code, testCase.response))
|
||||
}
|
||||
req := httptest.NewRequest(testCase.method, testCase.localPath, nil)
|
||||
req := httptest.NewRequest(testCase.method, testCase.localPath, strings.NewReader(dummySilence))
|
||||
resp := newCloseNotifyingRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
if resp.Code != testCase.code {
|
||||
@@ -182,6 +192,14 @@ var proxyHeaderTests = []proxyHeaderTest{
|
||||
}
|
||||
|
||||
func TestProxyHeaders(t *testing.T) {
|
||||
dummySilence := `{
|
||||
"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" }]
|
||||
}`
|
||||
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
@@ -224,7 +242,7 @@ func TestProxyHeaders(t *testing.T) {
|
||||
return httpmock.NewStringResponse(testCase.code, "ok"), nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(testCase.method, testCase.localPath, nil)
|
||||
req := httptest.NewRequest(testCase.method, testCase.localPath, strings.NewReader(dummySilence))
|
||||
resp := newCloseNotifyingRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
if resp.Code != testCase.code {
|
||||
@@ -277,6 +295,14 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
dummySilence := `{
|
||||
"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" }]
|
||||
}`
|
||||
|
||||
for _, testCase := range proxyTests {
|
||||
t.Run(fmt.Sprintf("prefix=%s|uri=%s", testCase.listenPrefix, testCase.alertmanagerURI), func(t *testing.T) {
|
||||
httpmock.Reset()
|
||||
@@ -301,7 +327,7 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
|
||||
return httpmock.NewStringResponse(200, "ok"), nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("POST", testCase.requestURI, nil)
|
||||
req := httptest.NewRequest("POST", testCase.requestURI, strings.NewReader(dummySilence))
|
||||
resp := newCloseNotifyingRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
if resp.Code != 200 {
|
||||
@@ -536,3 +562,476 @@ func TestProxyUserRewrite(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxySilenceACL(t *testing.T) {
|
||||
type proxyTest struct {
|
||||
name string
|
||||
|
||||
authGroups map[string][]string
|
||||
silenceACLs []*silenceACL
|
||||
|
||||
requestUsername string
|
||||
|
||||
frontednRequestBody string
|
||||
responseCode int
|
||||
}
|
||||
|
||||
defaultBody := `{
|
||||
"comment": "comment",
|
||||
"createdBy": "alice",
|
||||
"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)" }
|
||||
]}`
|
||||
|
||||
proxyTests := []proxyTest{
|
||||
{
|
||||
name: "no config, allowed",
|
||||
authGroups: map[string][]string{},
|
||||
silenceACLs: []*silenceACL{},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "matcher required, no match, allowed",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require cluster=dev",
|
||||
Scope: silenceACLScope{
|
||||
Groups: []string{"foo"},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "cluster",
|
||||
Value: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "matcher required, alertmanager mismatch, allowed",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require cluster=dev",
|
||||
Scope: silenceACLScope{
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{"proxyFoo"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "cluster",
|
||||
Value: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "matcher required, filter mismatch, allowed",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require cluster=dev",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{Name: "foo", Value: "bar"},
|
||||
},
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "cluster",
|
||||
Value: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "matcher required, match, blocked",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require cluster=dev",
|
||||
Scope: silenceACLScope{
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "cluster",
|
||||
Value: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
{
|
||||
name: "matcher required, all groups, match, blocked",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require cluster=dev",
|
||||
Scope: silenceACLScope{
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{"foo", "proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "cluster",
|
||||
Value: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
{
|
||||
name: "matcher required, filter match, blocked",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require cluster=dev",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{Name: "alertname", Value: "Fake Alert"},
|
||||
},
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "cluster",
|
||||
Value: "dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
{
|
||||
name: "matcher required, filter name regex match, blocked",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require foo=bar",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*")},
|
||||
},
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
{
|
||||
name: "matcher required, filter value regex match, blocked",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require foo=bar",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{Name: "alertname", ValueRegex: regexp.MustCompile(".*")},
|
||||
},
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
{
|
||||
name: "matcher required, match, allowed",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "requireMatcher",
|
||||
Reason: "require foo=~(bar|baz)",
|
||||
Scope: silenceACLScope{
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{"proxyACL"},
|
||||
},
|
||||
Matchers: aclMatchers{
|
||||
Required: []silenceMatcher{
|
||||
{
|
||||
Name: "alertname",
|
||||
Value: "Fake Alert",
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "(bar|baz)",
|
||||
IsRegex: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "block all regex silences",
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "block",
|
||||
Reason: "block all regex silences",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
|
||||
},
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
{
|
||||
name: "block all regex silences except for admins, admin user, allowed",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
"users": {"alice"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "allow",
|
||||
Reason: "block all regex silences",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
|
||||
},
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
},
|
||||
{
|
||||
Action: "block",
|
||||
Reason: "block all regex silences",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
|
||||
},
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "block all regex silences on alertname=Block, allowed",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
"users": {"alice"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "block",
|
||||
Reason: "block alertname=Block",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{Name: "alertname", Value: "Block"},
|
||||
},
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
},
|
||||
},
|
||||
requestUsername: "bob",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 200,
|
||||
},
|
||||
{
|
||||
name: "block all regex silences except for admins, non-admin user, blocked",
|
||||
authGroups: map[string][]string{
|
||||
"admins": {"bob"},
|
||||
"users": {"alice"},
|
||||
},
|
||||
silenceACLs: []*silenceACL{
|
||||
{
|
||||
Action: "allow",
|
||||
Reason: "block all regex silences",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
|
||||
},
|
||||
Groups: []string{"admins"},
|
||||
Alertmanagers: []string{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
},
|
||||
{
|
||||
Action: "block",
|
||||
Reason: "block all regex silences",
|
||||
Scope: silenceACLScope{
|
||||
Filters: []silenceFilter{
|
||||
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
|
||||
},
|
||||
Groups: []string{},
|
||||
Alertmanagers: []string{},
|
||||
},
|
||||
Matchers: aclMatchers{},
|
||||
},
|
||||
},
|
||||
requestUsername: "alice",
|
||||
frontednRequestBody: defaultBody,
|
||||
responseCode: 400,
|
||||
},
|
||||
}
|
||||
|
||||
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 = "X-User"
|
||||
config.Config.Authentication.Header.ValueRegex = "(.+)"
|
||||
|
||||
config.Config.Authorization.Groups = []config.AuthorizationGroup{}
|
||||
for groupName, members := range testCase.authGroups {
|
||||
g := config.AuthorizationGroup{Name: groupName, Members: members}
|
||||
config.Config.Authorization.Groups = append(config.Config.Authorization.Groups, g)
|
||||
}
|
||||
|
||||
silenceACLs = testCase.silenceACLs
|
||||
|
||||
r := ginTestEngine()
|
||||
|
||||
am, err := alertmanager.NewAlertmanager(
|
||||
"proxyACL",
|
||||
"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/proxyACL/api/v2/silences", ioutil.NopCloser(bytes.NewBufferString(testCase.frontednRequestBody)))
|
||||
req.Header.Set("X-User", testCase.requestUsername)
|
||||
|
||||
resp := newCloseNotifyingRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
if resp.Code != testCase.responseCode {
|
||||
t.Errorf("Got response code %d instead of %d", resp.Code, testCase.responseCode)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Config is valid with example silence ACL rules
|
||||
karma.bin-should-work --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
! stderr 'level=error'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: requireMatcher
|
||||
reason: require cluster=~dev|prod for admins
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
value_re: .+
|
||||
alertmanagers:
|
||||
- default
|
||||
groups:
|
||||
- admins
|
||||
matchers:
|
||||
required:
|
||||
- name: cluster
|
||||
value: dev|prod
|
||||
isRegex: true
|
||||
- action: block
|
||||
reason: block cluster=prod for users
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
value: prod
|
||||
groups:
|
||||
- users
|
||||
@@ -0,0 +1,30 @@
|
||||
# Raises an error if --authorization.acl points to a file that cannot be parsed
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Failed to parse silence ACL configuration file \\"acl.yaml\\": yaml: unmarshal errors:.*"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
This Is Not yaml
|
||||
@@ -0,0 +1,27 @@
|
||||
# Raises an error if --authorization.acl points to a missing file
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Failed to load silence ACL configuration file \\"acl.yaml\\": open acl.yaml: no such file or directory"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
@@ -0,0 +1,32 @@
|
||||
# Raises an error if silence ACL rule uses invalid 'action' value
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule requires ''action'' to be one of \[allow block requireMatcher\], got \\"foo\\""'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: foo
|
||||
reason: invalid action
|
||||
@@ -0,0 +1,35 @@
|
||||
# Raises an error if silence ACL rule uses invalid 'alertmanagers' value
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: invalid ACL rule, no alertmanager with name \\"unknown\\" found"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: invalid group
|
||||
scope:
|
||||
alertmanagers:
|
||||
- unknown
|
||||
@@ -0,0 +1,36 @@
|
||||
# Raises an error if silence ACL rule uses filter with invalid name_re
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: invalid ACL rule, failed to parse name_re \\"cluster\*\*\*\\": error parsing regexp: invalid nested repetition operator: `\*\*`"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: missing name
|
||||
scope:
|
||||
filters:
|
||||
- name_re: cluster***
|
||||
value: prod
|
||||
@@ -0,0 +1,36 @@
|
||||
# Raises an error if silence ACL rule uses filter with invalid value_re
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: invalid ACL rule, failed to parse value_re \\"prod\*\*\*\\": error parsing regexp: invalid nested repetition operator: `\*\*`"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: missing name
|
||||
scope:
|
||||
filters:
|
||||
- name_re: cluster[0-9]*
|
||||
value_re: prod***
|
||||
@@ -0,0 +1,35 @@
|
||||
# Raises an error if silence ACL rule uses invalid 'groups' value
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: invalid silence ACL rule, no group with name \\"unknown\\" found in authorization.groups configuration"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: invalid group
|
||||
scope:
|
||||
groups:
|
||||
- unknown
|
||||
@@ -0,0 +1,37 @@
|
||||
# Raises an error if silence ACL rule uses filter with both name and name_re
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter can only have ''name'' or ''name_re'' set, not both"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: missing name
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
name_re: cluster
|
||||
value: prod
|
||||
@@ -0,0 +1,37 @@
|
||||
# Raises an error if silence ACL rule uses filter with both value and value_re
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter can only have ''value'' or ''value_re'' set, not both"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: missing name
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
value: prod
|
||||
value_re: prod
|
||||
@@ -0,0 +1,35 @@
|
||||
# Raises an error if silence ACL rule uses filter with missing name or name_re
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter requires ''name'' or ''name_re'' to be set"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: missing name
|
||||
scope:
|
||||
filters:
|
||||
- value: prod
|
||||
@@ -0,0 +1,35 @@
|
||||
# Raises an error if silence ACL rule uses filter with missing value or value_re
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter requires ''value'' or ''value_re'' to be set"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
reason: missing value
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
@@ -0,0 +1,35 @@
|
||||
# Raises an error if silence ACL rule uses matcher without name
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule matcher requires ''name'' to be set"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: requireMatcher
|
||||
reason: missing name
|
||||
matchers:
|
||||
required:
|
||||
- value: prod
|
||||
@@ -0,0 +1,35 @@
|
||||
# Raises an error if silence ACL rule uses matcher without value
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule matcher requires ''value'' to be set"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: requireMatcher
|
||||
reason: missing value
|
||||
matchers:
|
||||
required:
|
||||
- name: cluster
|
||||
@@ -0,0 +1,31 @@
|
||||
# Raises an error if silence ACL rule is missing 'reason'
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
! stderr 'msg="Configuration is valid"'
|
||||
stderr 'msg="Reading silence ACL config file acl.yaml"'
|
||||
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule requires ''reason'' to be set"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
acl:
|
||||
silences: acl.yaml
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
|
||||
-- acl.yaml --
|
||||
rules:
|
||||
- action: block
|
||||
@@ -0,0 +1,17 @@
|
||||
# Raises an error if authorization group is missing name
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
stderr 'msg="''members'' is required for every authorization group"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
@@ -0,0 +1,22 @@
|
||||
# Raises an error if authorization group is missing name
|
||||
karma.bin-should-fail --log.format=text --log.config=false --check-config
|
||||
! stdout .
|
||||
stderr 'msg="''name'' is required for every authorization group"'
|
||||
|
||||
-- karma.yaml --
|
||||
authentication:
|
||||
header:
|
||||
name: "X-User"
|
||||
value_re: "(.+)"
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- members:
|
||||
- john
|
||||
alertmanager:
|
||||
servers:
|
||||
- name: default
|
||||
uri: https://localhost:9093
|
||||
@@ -75,6 +75,10 @@ level=info msg=" name: \"\""
|
||||
level=info msg=" value_re: \"\""
|
||||
level=info msg=" basicAuth:"
|
||||
level=info msg=" users: []"
|
||||
level=info msg="authorization:"
|
||||
level=info msg=" groups: []"
|
||||
level=info msg=" acl:"
|
||||
level=info msg=" silences: \"\""
|
||||
level=info msg="alertmanager:"
|
||||
level=info msg=" interval: 10s"
|
||||
level=info msg=" servers:"
|
||||
|
||||
@@ -11,6 +11,12 @@ authentication:
|
||||
password: 1234
|
||||
- username: string
|
||||
password: '1234'
|
||||
authorization:
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
alertmanager:
|
||||
interval: 10s
|
||||
servers:
|
||||
@@ -244,6 +250,14 @@ level=info msg=" - username: number"
|
||||
level=info msg=" password: '***'"
|
||||
level=info msg=" - username: string"
|
||||
level=info msg=" password: '***'"
|
||||
level=info msg="authorization:"
|
||||
level=info msg=" groups:"
|
||||
level=info msg=" - name: admins"
|
||||
level=info msg=" members:"
|
||||
level=info msg=" - alice"
|
||||
level=info msg=" - bob"
|
||||
level=info msg=" acl:"
|
||||
level=info msg=" silences: \"\""
|
||||
level=info msg="alertmanager:"
|
||||
level=info msg=" interval: 10s"
|
||||
level=info msg=" servers:"
|
||||
@@ -357,18 +371,14 @@ level=info msg=" - strip2"
|
||||
level=info msg=" color:"
|
||||
level=info msg=" custom:"
|
||||
level=info msg=" region:"
|
||||
level=info msg=" - value: \"\""
|
||||
level=info msg=" value_re: .*"
|
||||
level=info msg=" - value_re: .*"
|
||||
level=info msg=" color: '#736598'"
|
||||
level=info msg=" severity:"
|
||||
level=info msg=" - value: info"
|
||||
level=info msg=" value_re: \"\""
|
||||
level=info msg=" color: '#87c4e0'"
|
||||
level=info msg=" - value: warning"
|
||||
level=info msg=" value_re: \"\""
|
||||
level=info msg=" color: '#ffae42'"
|
||||
level=info msg=" - value: critical"
|
||||
level=info msg=" value_re: \"\""
|
||||
level=info msg=" color: '#ff220c'"
|
||||
level=info msg=" static:"
|
||||
level=info msg=" - job"
|
||||
|
||||
@@ -7,18 +7,14 @@ stderr 'msg=" strip: \[\]"'
|
||||
stderr 'msg=" color:"'
|
||||
stderr 'msg=" custom:"'
|
||||
stderr 'msg=" region:"'
|
||||
stderr 'msg=" - value: \\"\\""'
|
||||
stderr 'msg=" value_re: .*"'
|
||||
stderr 'msg=" - value_re: .*"'
|
||||
stderr 'msg=" color: ''#736598''"'
|
||||
stderr 'msg=" severity:"'
|
||||
stderr 'msg=" - value: P3"'
|
||||
stderr 'msg=" value_re: \\"\\""'
|
||||
stderr 'msg=" color: ''#87c4e0''"'
|
||||
stderr 'msg=" - value: P2"'
|
||||
stderr 'msg=" value_re: \\"\\""'
|
||||
stderr 'msg=" color: ''#ffae42''"'
|
||||
stderr 'msg=" - value: P1"'
|
||||
stderr 'msg=" value_re: \\"\\""'
|
||||
stderr 'msg=" color: ''#ff220c''"'
|
||||
! stderr 'level=error'
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ COPY demo/alertmanager.yaml /etc/alertmanager.yaml
|
||||
COPY demo/generator.py /generator.py
|
||||
COPY --from=go-builder /src/karma /karma
|
||||
COPY demo/karma.yaml /etc/karma.yaml
|
||||
COPY demo/acls.yaml /etc/acls.yaml
|
||||
COPY demo/custom.js /custom.js
|
||||
RUN adduser -D karma
|
||||
USER karma
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
rules:
|
||||
- action: block
|
||||
reason: dev cluster cannot be silenced
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
value: dev
|
||||
@@ -1,3 +1,6 @@
|
||||
authorization:
|
||||
acl:
|
||||
silences: /etc/acls.yaml
|
||||
alertmanager:
|
||||
interval: 10s
|
||||
servers:
|
||||
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
# Silence Access Control Lists
|
||||
|
||||
## Intro
|
||||
|
||||
Karma provides ability to setup ACLs for silences created by users. This can be
|
||||
used to limit what kind of silences each user is allowed to create, which can
|
||||
help to avoid, for example, `Team A` accidentally silencing alerts for `Team B`,
|
||||
or blocking engineering team from creating any silence at all, leaving that
|
||||
ability only to the sys admin / SRE team.
|
||||
|
||||
Example Alertmanager silence:
|
||||
|
||||
```YAML
|
||||
{
|
||||
"matchers": [
|
||||
{
|
||||
"name": "alertname",
|
||||
"value": "Test Alert",
|
||||
"isRegex": false
|
||||
},
|
||||
{
|
||||
"name": "cluster",
|
||||
"value": "prod",
|
||||
"isRegex": false
|
||||
},
|
||||
{
|
||||
"name": "instance",
|
||||
"value": "server1",
|
||||
"isRegex": false
|
||||
}
|
||||
],
|
||||
"startsAt": "2020-03-09T20:11:00.000Z",
|
||||
"endsAt": "2020-03-09T21:11:00.000Z",
|
||||
"createdBy": "me@example.com",
|
||||
"comment": "Silence Test Alert on server1"
|
||||
}
|
||||
```
|
||||
|
||||
It would be applied to all alerts with name `Test Alert` and where label
|
||||
`cluster` is equal to `prod`.
|
||||
An ACL rule could be used to restrict silence creation based on matched labels,
|
||||
so for example only selected users would be allowed to silence this specific
|
||||
alert.
|
||||
|
||||
## Requirements
|
||||
|
||||
For ACLs to work a few configuring options are required:
|
||||
|
||||
- `authorization:acl:silences` is set with acl config file path, there's no
|
||||
support for configuring ACLs via environment variables
|
||||
- `proxy` must be enabled in karma configuration for each Alertmanager server
|
||||
where ACLs will be applied. `proxy: true` tells karma UI to proxy all silence
|
||||
operation requests (creating, editing & deleting silences) via karma backend.
|
||||
Since ACLs are applied in the proxy code it needs to be enabled to take
|
||||
effect. It is recommended to block ability for users to connect directly to
|
||||
Alertmanager servers to avoid bypassing ACL rules (alertmanager accepts all
|
||||
silences).
|
||||
|
||||
Optional configuration:
|
||||
|
||||
- `authentication` if configured user based matching of ACLs can be used,
|
||||
Header authentication with a frontend authentication proxy that passes
|
||||
usernames via header is recommended. This can be done with nginx configured
|
||||
as an authentication reverse proxy or proxy services like Cloudflare Access.
|
||||
- `authorization:groups` must be configured if group polices will be used.
|
||||
This configuration maps users into groups, allowing to use those groups in
|
||||
ACL rules.
|
||||
|
||||
## Regex silences
|
||||
|
||||
Alertmanager silences allow to use regex rules which can make it tricky to apply
|
||||
ACLs to those silences.
|
||||
|
||||
Silence example using regex:
|
||||
|
||||
```YAML
|
||||
{
|
||||
"matchers": [
|
||||
{
|
||||
"name": "alertname",
|
||||
"value": "Test Alert",
|
||||
"isRegex": false
|
||||
},
|
||||
{
|
||||
"name": "cluster",
|
||||
"value": "staging|prod",
|
||||
"isRegex": true
|
||||
}
|
||||
],
|
||||
"startsAt": "2020-03-09T20:11:00.000Z",
|
||||
"endsAt": "2020-03-09T21:11:00.000Z",
|
||||
"createdBy": "me@example.com",
|
||||
"comment": "Silence Test Alert in staging & prod cluster"
|
||||
}
|
||||
```
|
||||
|
||||
The difference compared to the previous example is that the `cluster` label
|
||||
is now matched using `staging|prod` regex, so any alert with `cluster` label
|
||||
equal to `staging` or `prod` will be matched.
|
||||
This is a simple example, regexes allow to create very complex matching
|
||||
rules.
|
||||
|
||||
The effect on ACL rules can be illustrated with this example: let's say we have
|
||||
a group that should never be allowed to create any silence for `prod` cluster,
|
||||
so a silence like the one below should be blocked:
|
||||
|
||||
```YAML
|
||||
{
|
||||
"matchers": [
|
||||
{
|
||||
"name": "alertname",
|
||||
"value": "Test Alert",
|
||||
"isRegex": false
|
||||
},
|
||||
{
|
||||
"name": "cluster",
|
||||
"value": "prod",
|
||||
"isRegex": false
|
||||
}
|
||||
],
|
||||
"startsAt": "2020-03-09T20:11:00.000Z",
|
||||
"endsAt": "2020-03-09T21:11:00.000Z",
|
||||
"createdBy": "me@example.com",
|
||||
"comment": "Silence Test Alert in prod cluster"
|
||||
}
|
||||
```
|
||||
|
||||
But if we would create an ACL rule that simply blocks silences with matcher:
|
||||
|
||||
```YAML
|
||||
{
|
||||
"name": "cluster",
|
||||
"value": "prod",
|
||||
"isRegex": false
|
||||
}
|
||||
```
|
||||
|
||||
then any user could bypass that with a regex matcher like:
|
||||
|
||||
```YAML
|
||||
{
|
||||
"name": "cluster",
|
||||
"value": "pro[d]",
|
||||
"isRegex": true
|
||||
}
|
||||
```
|
||||
|
||||
Because of that it is *highly recommended* to block regex silences, which can
|
||||
be done with an ACL rule. Since rules are evaluated in the order they are
|
||||
listed in the config file it is best to set this as the very first rule.
|
||||
See examples below to learn how to block regex silences.
|
||||
|
||||
## Configuration syntax
|
||||
|
||||
- `rules` - list of silence ACL rules, rules are evaluated in the order they
|
||||
appear in this list
|
||||
|
||||
Rule syntax:
|
||||
|
||||
```YAML
|
||||
action: string
|
||||
reason: string
|
||||
scope:
|
||||
groups: list of strings
|
||||
alertmanagers: list of strings
|
||||
filters: list of filters
|
||||
matchers:
|
||||
required: list of silence matchers
|
||||
```
|
||||
|
||||
- `action` - this is the name of the action to take if given ACL matches all
|
||||
the conditions.
|
||||
Valid actions are:
|
||||
- `allow` - skip all other ACLs and allow silence to be created
|
||||
- `block` - skip all other ACLs and block silences from being created
|
||||
- `requireMatcher` - block silence if it doesn't have all of matchers
|
||||
specified in `matchers:required`
|
||||
- `reason` - message that will be returned to the user if this ACL blocks any
|
||||
silence
|
||||
- `scope` - this section contains all conditions required to apply given ACL
|
||||
rule to specific silence, if it's skipped then ACL rule will be applied to
|
||||
all users and every silence
|
||||
- `scope:groups` - list of group names from `authorization:groups`, if no group
|
||||
is specified here then this ACL will be applied to all users
|
||||
- `scope:alertmanagers` - list of alertmanager names as specified in
|
||||
`alertmanager:servers`, if no name is specified here then this ACL will be
|
||||
applied to silences for all alertmanager servers
|
||||
- `scope:filters` - list of matcher filters evaluated when checking if this ACL
|
||||
should be applied to given silence. Those filters can be used to enforce
|
||||
ACL rules only to some silences and are compared against silence matchers.
|
||||
Syntax:
|
||||
|
||||
```YAML
|
||||
name: string
|
||||
name_re: regex
|
||||
value: string
|
||||
value_re: regex
|
||||
isRegex: bool
|
||||
```
|
||||
|
||||
Every rule must have `name` or `name_re` AND `value` or `value_re`, default
|
||||
value for `isRegex` is `false`.
|
||||
Filter works by comparing `name` and `name_re` with silence matcher `name`,
|
||||
`value` and `value_re` with silence matcher `value` and `isRegex` on the
|
||||
filter with `isRegex` on silence matcher. See examples below.
|
||||
- `matchers:required` - list of additional matchers that must be part of the
|
||||
silence if it matches groups, alertmanagers and filters. This is only used
|
||||
if `action` is set to `requireMatcher`.
|
||||
Syntax for each matcher:
|
||||
|
||||
```YAML
|
||||
name: string
|
||||
value: string or regex
|
||||
isRegex: bool
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Block silences using regex matchers
|
||||
|
||||
This rule will match all silences with any matcher using regexes
|
||||
(`isRegex: true` on the matcher) and block it.
|
||||
|
||||
```YAML
|
||||
rules:
|
||||
- action: block
|
||||
reason: all regex silences are blocked, use only concrete label names and values
|
||||
scope:
|
||||
filters:
|
||||
- name_re: .+
|
||||
value_re: .+
|
||||
isRegex: true
|
||||
```
|
||||
|
||||
### Allow group to create any silence
|
||||
|
||||
```YAML
|
||||
rules:
|
||||
- action: allow
|
||||
reason: admins are allowed
|
||||
scope:
|
||||
groups:
|
||||
- admins
|
||||
```
|
||||
|
||||
### Allow only admins group to create silences with cluster=prod
|
||||
|
||||
First allow all members of the `admins` group to create any silence, then block
|
||||
silences with `cluster=prod`. Since ACL rules are evaluated in the order
|
||||
specified and first `allow` or `block` rule stops other rule processing this
|
||||
will allow `admins` to create `cluster=prod` silences while everyone else is
|
||||
blocked from it. Disabling regex rules as first steps prevents users from
|
||||
bypassing those ACLs with regex silences.
|
||||
|
||||
```YAML
|
||||
rules:
|
||||
- action: block
|
||||
reason: all regex silences are blocked, use only concrete label names and values
|
||||
scope:
|
||||
filters:
|
||||
- name_re: .+
|
||||
value_re: .+
|
||||
isRegex: true
|
||||
- action: allow
|
||||
reason: admins are allowed
|
||||
scope:
|
||||
groups:
|
||||
- admins
|
||||
- action: block
|
||||
reason: only admins can create silences with cluster=prod
|
||||
scope:
|
||||
filters:
|
||||
- name: cluster
|
||||
value: prod
|
||||
```
|
||||
|
||||
### Require postgresAdmins group to always specify db=postgres in silences
|
||||
|
||||
Block `postgresAdmins` members from creating silences unless they add
|
||||
`db=postgres` to the list of matchers.
|
||||
|
||||
```YAML
|
||||
rules:
|
||||
- action: requireMatcher
|
||||
reason: postgres admins must add db=postgres to all silences
|
||||
scope:
|
||||
groups:
|
||||
- postgresAdmins
|
||||
matchers:
|
||||
required:
|
||||
- name: db
|
||||
value: postgres
|
||||
```
|
||||
+47
-6
@@ -38,6 +38,7 @@ There are currently two supported authentication methods:
|
||||
Only one method can be enabled in the config.
|
||||
Enabling authentication will also force silences to be created with usernames
|
||||
passed from credentials.
|
||||
Syntax:
|
||||
|
||||
```YAML
|
||||
authentication:
|
||||
@@ -97,6 +98,52 @@ authentication:
|
||||
value_re: ^(.+)$
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
`authorization` section allows to configure authorization groups used in
|
||||
silence ACL rules.
|
||||
Syntax:
|
||||
|
||||
```YAML
|
||||
authorization:
|
||||
acl:
|
||||
silences: string
|
||||
groups:
|
||||
- name: string
|
||||
members: list of strings
|
||||
```
|
||||
|
||||
- `acl:silences` - path to silence ACL configuration file, see
|
||||
[ACLs](/docs/ACLs.md) for details
|
||||
- `groups` - list of group definitons, each group must have a `name` and
|
||||
`members` list. `name` will be used in silence ACL rules, `members` list
|
||||
should contain list of user names as passed from authentication layer.
|
||||
|
||||
Example with two groups using basic auth users and silences ACL config:
|
||||
|
||||
```YAML
|
||||
authentication:
|
||||
basicAuth:
|
||||
users:
|
||||
- username: alice
|
||||
password: secret
|
||||
- username: bob
|
||||
password: secret
|
||||
- username: john
|
||||
password: secret
|
||||
authorization:
|
||||
acls:
|
||||
silences: /etc/karma/acls.yaml
|
||||
groups:
|
||||
- name: admins
|
||||
members:
|
||||
- alice
|
||||
- bob
|
||||
- name: users
|
||||
members:
|
||||
- john
|
||||
```
|
||||
|
||||
### Alertmanagers
|
||||
|
||||
`alertmanager` section allows setting Alertmanager servers that should be
|
||||
@@ -824,12 +871,6 @@ silences:
|
||||
uriTemplate: https://jira.example.com/browse/$1
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
```YAML
|
||||
jira: []
|
||||
```
|
||||
|
||||
### Receivers
|
||||
|
||||
`receivers` section allows configuring how alerts from different receivers are
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type SilenceMatcher struct {
|
||||
Name string `yaml:"name"`
|
||||
Value string `yaml:"value"`
|
||||
IsRegex bool `yaml:"isRegex"`
|
||||
}
|
||||
|
||||
type SilenceACLMatchersConfig struct {
|
||||
Required []SilenceMatcher `yaml:"required"`
|
||||
}
|
||||
|
||||
type SilenceFilters struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
NameRegex string `yaml:"name_re,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
ValueRegex string `yaml:"value_re,omitempty"`
|
||||
IsRegex bool `yaml:"isRegex"`
|
||||
}
|
||||
|
||||
type SilenceACLRuleScope struct {
|
||||
Groups []string
|
||||
Alertmanagers []string
|
||||
Filters []SilenceFilters
|
||||
}
|
||||
|
||||
type SilenceACLRule struct {
|
||||
Action string
|
||||
Reason string
|
||||
Scope SilenceACLRuleScope
|
||||
Matchers SilenceACLMatchersConfig
|
||||
}
|
||||
|
||||
type silencesACLSchema struct {
|
||||
Rules []SilenceACLRule
|
||||
}
|
||||
|
||||
func ReadSilenceACLConfig(path string) (*silencesACLSchema, error) {
|
||||
cfg := silencesACLSchema{}
|
||||
|
||||
f, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to load silence ACL configuration file %q: %v", path, err)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(f, &cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse silence ACL configuration file %q: %v", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
@@ -58,6 +58,8 @@ func SetupFlags(f *pflag.FlagSet) {
|
||||
f.String("alertAcknowledgement.author", "karma", "Default silence author when acknowledging alerts with short lived silences")
|
||||
f.String("alertAcknowledgement.commentPrefix", "ACK!", "Comment prefix used when acknowledging alerts with short lived silences")
|
||||
|
||||
f.String("authorization.acl.silences", "", "Path to silence ACL config file")
|
||||
|
||||
f.Bool(
|
||||
"annotations.default.hidden", false,
|
||||
"Hide all annotations by default unless explicitly listed in the 'visible' list")
|
||||
@@ -313,6 +315,15 @@ func (config *configSchema) Read(flags *pflag.FlagSet) string {
|
||||
}
|
||||
}
|
||||
|
||||
for _, authGroup := range config.Authorization.Groups {
|
||||
if authGroup.Name == "" {
|
||||
log.Fatalf("'name' is required for every authorization group")
|
||||
}
|
||||
if len(authGroup.Members) == 0 {
|
||||
log.Fatalf("'members' is required for every authorization group")
|
||||
}
|
||||
}
|
||||
|
||||
for labelName, customColors := range config.Labels.Color.Custom {
|
||||
for i, customColor := range customColors {
|
||||
if customColor.Value == "" && customColor.ValueRegex == "" {
|
||||
|
||||
@@ -26,6 +26,10 @@ func testReadConfig(t *testing.T) {
|
||||
value_re: ""
|
||||
basicAuth:
|
||||
users: []
|
||||
authorization:
|
||||
groups: []
|
||||
acl:
|
||||
silences: ""
|
||||
alertmanager:
|
||||
interval: 1s
|
||||
servers:
|
||||
|
||||
@@ -32,8 +32,8 @@ type LinkDetectRules struct {
|
||||
}
|
||||
|
||||
type CustomLabelColor struct {
|
||||
Value string `yaml:"value"`
|
||||
ValueRegex string `yaml:"value_re" koanf:"value_re"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
ValueRegex string `yaml:"value_re,omitempty" koanf:"value_re"`
|
||||
CompiledRegex *regexp.Regexp `yaml:"-"`
|
||||
Color string `yaml:"color"`
|
||||
}
|
||||
@@ -45,6 +45,11 @@ type AuthenticationUser struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
type AuthorizationGroup struct {
|
||||
Name string
|
||||
Members []string
|
||||
}
|
||||
|
||||
type configSchema struct {
|
||||
Authentication struct {
|
||||
Enabled bool `yaml:"-" koanf:"-"`
|
||||
@@ -56,6 +61,12 @@ type configSchema struct {
|
||||
Users []AuthenticationUser
|
||||
} `yaml:"basicAuth" koanf:"basicAuth"`
|
||||
}
|
||||
Authorization struct {
|
||||
Groups []AuthorizationGroup
|
||||
ACL struct {
|
||||
Silences string
|
||||
} `yaml:"acl" koanf:"acl"`
|
||||
}
|
||||
Alertmanager struct {
|
||||
Interval time.Duration
|
||||
Servers []AlertmanagerConfig
|
||||
|
||||
@@ -31,6 +31,7 @@ type SilenceMapper interface {
|
||||
Mapper
|
||||
Collect(string, map[string]string, time.Duration, http.RoundTripper) ([]models.Silence, error)
|
||||
RewriteUsername([]byte, string) ([]byte, error)
|
||||
Unmarshal([]byte) (*models.Silence, error)
|
||||
}
|
||||
|
||||
// StatusMapper handles mapping Alertmanager status information containing cluster config
|
||||
|
||||
@@ -130,3 +130,30 @@ func rewriteSilenceUsername(body []byte, username string) ([]byte, error) {
|
||||
s.CreatedBy = &username
|
||||
return s.MarshalBinary()
|
||||
}
|
||||
|
||||
func unmarshal(body []byte) (*models.Silence, error) {
|
||||
s := ammodels.PostableSilence{}
|
||||
|
||||
err := s.UnmarshalBinary(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
us := models.Silence{
|
||||
ID: s.ID,
|
||||
StartsAt: time.Time(*s.StartsAt),
|
||||
EndsAt: time.Time(*s.EndsAt),
|
||||
CreatedBy: *s.CreatedBy,
|
||||
Comment: *s.Comment,
|
||||
}
|
||||
for _, m := range s.Matchers {
|
||||
sm := models.SilenceMatcher{
|
||||
Name: *m.Name,
|
||||
Value: *m.Value,
|
||||
IsRegex: *m.IsRegex,
|
||||
}
|
||||
us.Matchers = append(us.Matchers, sm)
|
||||
}
|
||||
|
||||
return &us, nil
|
||||
}
|
||||
|
||||
@@ -31,3 +31,7 @@ func (m SilenceMapper) Collect(uri string, headers map[string]string, timeout ti
|
||||
func (m SilenceMapper) RewriteUsername(body []byte, username string) ([]byte, error) {
|
||||
return rewriteSilenceUsername(body, username)
|
||||
}
|
||||
|
||||
func (m SilenceMapper) Unmarshal(body []byte) (*models.Silence, error) {
|
||||
return unmarshal(body)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user