feat(backend): add basic auth support

This commit is contained in:
Łukasz Mierzwa
2020-02-21 15:32:28 +00:00
parent b437685c85
commit e08c442e39
10 changed files with 119 additions and 21 deletions

View File

@@ -98,16 +98,28 @@ func setupRouter(router *gin.Engine) {
ExposeHeaders: []string{"Content-Length"},
}))
router.GET(getViewURL("/"), index)
router.GET(getViewURL("/health"), pong)
router.GET(getViewURL("/alerts.json"), alerts)
router.GET(getViewURL("/autocomplete.json"), autocomplete)
router.GET(getViewURL("/labelNames.json"), knownLabelNames)
router.GET(getViewURL("/labelValues.json"), knownLabelValues)
router.GET(getViewURL("/silences.json"), silences)
var protected *gin.RouterGroup
if len(config.Config.Authentication.Users) > 0 {
users := map[string]string{}
for _, u := range config.Config.Authentication.Users {
users[u.Username] = u.Password
}
protected = router.Group(getViewURL("/"), gin.BasicAuth(users))
} else {
protected = router.Group(getViewURL("/"))
}
router.GET(getViewURL("/custom.css"), customCSS)
router.GET(getViewURL("/custom.js"), customJS)
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)
protected.GET("/custom.css", customCSS)
protected.GET("/custom.js", customJS)
router.NoRoute(notFound)
}

View File

@@ -69,6 +69,8 @@ cmp stderr expected.stderr
-- expected.stderr --
level=info msg="Version: dev"
level=info msg="Parsed configuration:"
level=info msg="authentication:"
level=info msg=" users: []"
level=info msg="alertmanager:"
level=info msg=" interval: 10s"
level=info msg=" servers:"

View File

@@ -4,6 +4,12 @@ karma.bin-should-work --config.file=custom.yaml --check-config
cmp stderr expected.stderr
-- custom.yaml --
authentication:
users:
- username: number
password: 1234
- username: string
password: '1234'
alertmanager:
interval: 10s
servers:
@@ -231,6 +237,12 @@ FLR1flnW2lx5o5csDzTpi+jgC6nu1zE0DWo1c5ZdpVO289POIpqh
level=info msg="Reading configuration file custom.yaml"
level=info msg="Version: dev"
level=info msg="Parsed configuration:"
level=info msg="authentication:"
level=info msg=" users:"
level=info msg=" - username: number"
level=info msg=" password: '***'"
level=info msg=" - username: string"
level=info msg=" password: '***'"
level=info msg="alertmanager:"
level=info msg=" interval: 10s"
level=info msg=" servers:"

View File

@@ -143,6 +143,11 @@ func alerts(c *gin.Context) {
start := time.Now()
ts, _ := start.UTC().MarshalText()
var username string
if len(config.Config.Authentication.Users) > 0 {
username = c.MustGet(gin.AuthUserKey).(string)
}
// initialize response object, set fields that don't require any locking
resp := models.AlertsResponse{}
resp.Status = "success"
@@ -175,6 +180,10 @@ func alerts(c *gin.Context) {
CommentPrefix: config.Config.AlertAcknowledgement.CommentPrefix,
},
}
resp.Authentication = models.AuthenticationInfo{
Enabled: len(config.Config.Authentication.Users) > 0,
Username: username,
}
if config.Config.Grid.Sorting.CustomValues.Labels != nil {
resp.Settings.Sorting.ValueMapping = config.Config.Grid.Sorting.CustomValues.Labels

View File

@@ -818,3 +818,18 @@ func TestEmptySettings(t *testing.T) {
t.Errorf("Wrong settings returned (-want +got):\n%s", diff)
}
}
func TestBasicAuth(t *testing.T) {
config.Config.Authentication.Users = []config.AuthenticationUser{
{Username: "john", Password: "foobar"},
}
r := ginTestEngine()
for _, path := range []string{"/", "/alerts.json", "/autocomplete.json"} {
req := httptest.NewRequest("GET", path, nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != 401 {
t.Errorf("Expected 401 from %s, got %d", path, resp.Code)
}
}
}

View File

@@ -22,6 +22,22 @@ Example with environment variables:
CONFIG_FILE="docs/example.yaml"
```
### Authentication
`authentication` sections allows enabling authentication support in karma.
When set users will be require to authenticate to access karma.
Enabling authentication will also force silences to be created with usernames
passed from credentials.
```YAML
authentication:
users:
- username: string
password: string
```
- `authentication:users` - list of users (username & password) allowed to login.
### Alertmanagers
`alertmanager` section allows setting Alertmanager servers that should be

View File

@@ -265,6 +265,12 @@ func (config *configSchema) Read(flags *pflag.FlagSet) string {
config.SilenceForm.Strip.Labels = []string{}
}
for _, u := range config.Authentication.Users {
if u.Username == "" || u.Password == "" {
log.Fatalf("authentication.users require both username and password to be set")
}
}
if config.SilenceForm.Author.PopulateFromHeader.ValueRegex != "" {
_, err = regexp.Compile(config.SilenceForm.Author.PopulateFromHeader.ValueRegex)
if err != nil {
@@ -345,6 +351,16 @@ func (config *configSchema) LogValues() {
// make a copy of our config so we can edit it
cfg := configSchema(*config)
auth := []AuthenticationUser{}
for _, u := range cfg.Authentication.Users {
uu := AuthenticationUser{
Username: u.Username,
Password: "***",
}
auth = append(auth, uu)
}
cfg.Authentication.Users = auth
// replace passwords in Alertmanager URIs with 'xxx'
servers := []AlertmanagerConfig{}
for _, s := range cfg.Alertmanager.Servers {

View File

@@ -20,7 +20,9 @@ func resetEnv() {
}
func testReadConfig(t *testing.T) {
expectedConfig := `alertmanager:
expectedConfig := `authentication:
users: []
alertmanager:
interval: 1s
servers:
- name: default

View File

@@ -40,7 +40,15 @@ type CustomLabelColor struct {
type CustomLabelColors map[string][]CustomLabelColor
type AuthenticationUser struct {
Username string
Password string
}
type configSchema struct {
Authentication struct {
Users []AuthenticationUser
}
Alertmanager struct {
Interval time.Duration
Servers []AlertmanagerConfig

View File

@@ -296,19 +296,25 @@ type Settings struct {
AlertAcknowledgement AlertAcknowledgementSettings `json:"alertAcknowledgement"`
}
type AuthenticationInfo struct {
Enabled bool `json:"enabled"`
Username string `json:"username"`
}
// AlertsResponse is the structure of JSON response UI will use to get alert data
type AlertsResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
Upstreams AlertmanagerAPISummary `json:"upstreams"`
Silences map[string]map[string]Silence `json:"silences"`
AlertGroups []APIAlertGroup `json:"groups"`
TotalAlerts int `json:"totalAlerts"`
Colors LabelsColorMap `json:"colors"`
Filters []Filter `json:"filters"`
Counters LabelNameStatsList `json:"counters"`
Settings Settings `json:"settings"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
Upstreams AlertmanagerAPISummary `json:"upstreams"`
Silences map[string]map[string]Silence `json:"silences"`
AlertGroups []APIAlertGroup `json:"groups"`
TotalAlerts int `json:"totalAlerts"`
Colors LabelsColorMap `json:"colors"`
Filters []Filter `json:"filters"`
Counters LabelNameStatsList `json:"counters"`
Settings Settings `json:"settings"`
Authentication AuthenticationInfo `json:"authentication"`
}
// Autocomplete is the structure of autocomplete object for filter hints