fix(auth): add missing test coverage

This commit is contained in:
Łukasz Mierzwa
2021-10-23 14:38:54 +01:00
committed by Łukasz Mierzwa
parent 9b89b4ee2a
commit 4ccc0a35ab
19 changed files with 299 additions and 30 deletions
+4
View File
@@ -6,6 +6,10 @@
- Toast popup messages didn't respond to clicks.
### Added
- Loading user groups from HTTP headers, #3361 (@supertassu).
## v0.92
### Fixed
+22 -14
View File
@@ -22,6 +22,20 @@ func userGroups(username string) []string {
return groups
}
func groupsFromHeaders(r *http.Request, groupName, groupValueRegex, groupValueSeparator string) []string {
groups := []string{}
groupRegex := regex.MustCompileAnchored(groupValueRegex)
rawGroups := groupRegex.FindAllStringSubmatch(r.Header.Get(groupName), 1)
if len(rawGroups) > 0 && len(rawGroups[0]) > 1 {
for _, group := range strings.Split(rawGroups[0][1], groupValueSeparator) {
if v := strings.TrimSpace(group); v != "" {
groups = append(groups, v)
}
}
}
return groups
}
func headerAuth(name, valueRegex, groupName, groupValueRegex, groupValueSeparator string, allowBypass []string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -49,18 +63,7 @@ func headerAuth(name, valueRegex, groupName, groupValueRegex, groupValueSeparato
groups := userGroups(userName)
if groupName != "" {
rawGroups := []string{r.Header.Get(groupName)}
if groupValueSeparator != "" {
rawGroups = strings.Split(rawGroups[0], groupValueSeparator)
}
groupRegex := regex.MustCompileAnchored(groupValueRegex)
for _, group := range rawGroups {
groupMatches := groupRegex.FindAllStringSubmatch(group, 1)
if len(groupMatches) != 0 && len(groupMatches[0]) > 1 {
groups = append(groups, groupMatches[0][1])
}
}
groups = append(groups, groupsFromHeaders(r, groupName, groupValueRegex, groupValueSeparator)...)
}
ctx := context.WithValue(r.Context(), authUserKey("user"), userName)
@@ -86,7 +89,7 @@ func getGroupsFromContext(r *http.Request) []string {
return groups.([]string)
}
func basicAuth(creds map[string]string, allowBypass []string) func(next http.Handler) http.Handler {
func basicAuth(creds map[string]string, groupName, groupValueRegex, groupValueSeparator string, allowBypass []string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if slices.StringInSlice(allowBypass, r.URL.Path) {
@@ -106,8 +109,13 @@ func basicAuth(creds map[string]string, allowBypass []string) func(next http.Han
return
}
groups := userGroups(user)
if groupName != "" {
groups = append(groups, groupsFromHeaders(r, groupName, groupValueRegex, groupValueSeparator)...)
}
ctx := context.WithValue(r.Context(), authUserKey("user"), user)
ctx = context.WithValue(ctx, authUserKey("groups"), userGroups(user))
ctx = context.WithValue(ctx, authUserKey("groups"), groups)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+7 -1
View File
@@ -132,7 +132,13 @@ func setupRouter(router *chi.Mux, historyPoller *historyPoller) {
for _, u := range config.Config.Authentication.BasicAuth.Users {
users[u.Username] = u.Password
}
router.Use(basicAuth(users, allowAuthBypass))
router.Use(basicAuth(
users,
config.Config.Authentication.Header.GroupName,
config.Config.Authentication.Header.GroupValueRegex,
config.Config.Authentication.Header.GroupValueSeparator,
allowAuthBypass,
))
}
if config.Config.Listen.Prefix != "/" {
@@ -85,7 +85,7 @@ level=info msg=" name: X-Auth"
level=info msg=" value_re: ^(.+)$"
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
@@ -11,9 +11,9 @@ level=info msg="authentication:"
level=info msg=" header:"
level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_name: X-Groups"
level=info msg=" group_value_re: .+"
level=info msg=" group_value_separator: ','"
level=info msg=" basicAuth:"
level=info msg=" users:"
level=info msg=" - username: number"
@@ -267,6 +267,10 @@ level=info msg="Setting up proxy endpoints" alertmanager=local
level=info msg="Configuration is valid"
-- custom.yaml --
authentication:
header:
group_name: X-Groups
group_value_re: .+
group_value_separator: ','
basicAuth:
users:
- username: number
@@ -13,7 +13,7 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
+1 -1
View File
@@ -13,7 +13,7 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
+1 -1
View File
@@ -13,7 +13,7 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
+1 -1
View File
@@ -15,7 +15,7 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
@@ -13,7 +13,7 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
@@ -13,7 +13,7 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" group_name: \"\""
level=info msg=" group_value_re: \"\""
level=info msg=" group_value_separator: \"\""
level=info msg=" group_value_separator: ' '"
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
@@ -0,0 +1,14 @@
karma.bin-should-fail --config.file=karma.yaml
! stdout .
cmp stderr stderr.txt
-- stderr.txt --
level=error msg="Execution failed" error="authentication.header.group_name is required when authentication.header.group_value_re is set"
-- karma.yaml --
alertmanager:
servers:
- name: default
uri: https://127.0.0.1:9093
authentication:
header:
group_value_re: "(.+)"
@@ -0,0 +1,15 @@
karma.bin-should-fail --config.file=karma.yaml
! stdout .
cmp stderr stderr.txt
-- stderr.txt --
level=error msg="Execution failed" error="invalid regex for authentication.header.group_value_re: error parsing regexp: invalid nested repetition operator: `++`"
-- karma.yaml --
alertmanager:
servers:
- name: default
uri: https://127.0.0.1:9093
authentication:
header:
group_name: X-Groups
group_value_re: "(.+++++.)"
@@ -0,0 +1,14 @@
karma.bin-should-fail --config.file=karma.yaml
! stdout .
cmp stderr stderr.txt
-- stderr.txt --
level=error msg="Execution failed" error="authentication.header.group_value_re is required when authentication.header.group_name is set"
-- karma.yaml --
alertmanager:
servers:
- name: default
uri: https://127.0.0.1:9093
authentication:
header:
group_name: X-Groups
+3
View File
@@ -163,8 +163,10 @@ func alerts(w http.ResponseWriter, r *http.Request) {
ts, _ := start.UTC().MarshalText()
var username string
var groups []string
if config.Config.Authentication.Enabled {
username = getUserFromContext(r)
groups = getGroupsFromContext(r)
}
upstreams := getUpstreams()
@@ -207,6 +209,7 @@ func alerts(w http.ResponseWriter, r *http.Request) {
resp.Authentication = models.AuthenticationInfo{
Enabled: config.Config.Authentication.Enabled,
Username: username,
Groups: groups,
}
if config.Config.Grid.Sorting.CustomValues.Labels != nil {
+168 -1
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -1140,12 +1141,16 @@ func TestAuthentication(t *testing.T) {
name string
headerName string
headerRe string
groupName string
groupRe string
groupSeparator string
basicAuthUsers []config.AuthenticationUser
requestHeaders map[string]string
requestBasicAuthUser string
requestBasicAuthPassword string
responseCode int
responseUsername string
responseGroups []string
}
authTests := []authTest{
@@ -1207,6 +1212,7 @@ func TestAuthentication(t *testing.T) {
requestBasicAuthPassword: "foobar",
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, missing header, 401",
@@ -1241,6 +1247,7 @@ func TestAuthentication(t *testing.T) {
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, header correct #2, 200",
@@ -1251,19 +1258,159 @@ func TestAuthentication(t *testing.T) {
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, no groups, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "(.+)",
groupSeparator: ",",
requestHeaders: map[string]string{
"X-Auth": "Username john",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, group present, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "(.+)",
groupSeparator: ",",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "foo",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{"foo"},
},
{
name: "header auth, unmatched groups, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "Groups: (.+)",
groupSeparator: ",",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "foo",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, empty groups, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "Groups: (.+)",
groupSeparator: ",",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "Groups:",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, empty groups with spaces, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "Groups: (.+)",
groupSeparator: ",",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "Groups: ",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{},
},
{
name: "header auth, multiple groups, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "(.+)",
groupSeparator: ",",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "foo,bar, baz baz ",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{"foo", "bar", "baz baz"},
},
{
name: "header auth, multiple groups separated by spaces, 200",
headerName: "X-Auth",
headerRe: "Username (.+)",
groupName: "X-Auth-Groups",
groupRe: "(.+)",
groupSeparator: " ",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "foo bar baz baz ",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{"foo", "bar", "baz", "baz"},
},
{
name: "header auth, only groups enabled, no basic auth, 200",
groupName: "X-Auth-Groups",
groupRe: "(.+)",
groupSeparator: " ",
requestHeaders: map[string]string{
"X-Auth": "Username john",
"X-Auth-Groups": "foo",
},
responseCode: 200,
responseUsername: "",
responseGroups: []string{},
},
{
name: "header auth, only groups enabled, basic auth, 200",
basicAuthUsers: []config.AuthenticationUser{
{Username: "john", Password: "foobar"},
},
requestBasicAuthUser: "john",
requestBasicAuthPassword: "foobar",
groupName: "X-Auth-Groups",
groupRe: "Groups (.+)",
groupSeparator: " ",
requestHeaders: map[string]string{
"X-Auth-Groups": "Groups foo bar",
},
responseCode: 200,
responseUsername: "john",
responseGroups: []string{"foo", "bar"},
},
}
zerolog.SetGlobalLevel(zerolog.FatalLevel)
for _, testCase := range authTests {
t.Run(testCase.name, func(t *testing.T) {
config.Config.Authentication.Header.Name = testCase.headerName
config.Config.Authentication.Header.ValueRegex = testCase.headerRe
config.Config.Authentication.Header.GroupName = testCase.groupName
config.Config.Authentication.Header.GroupValueRegex = testCase.groupRe
config.Config.Authentication.Header.GroupValueSeparator = testCase.groupSeparator
config.Config.Authentication.BasicAuth.Users = testCase.basicAuthUsers
r := testRouter()
setupRouter(r, nil)
mockCache()
for _, path := range []string{
"/",
"/alerts.json",
"/alertList.json",
"/autocomplete.json?term=foo",
"/labelNames.json",
@@ -1276,7 +1423,23 @@ func TestAuthentication(t *testing.T) {
"/metrics",
"/metrics?bar=foo",
} {
req := httptest.NewRequest("GET", path, nil)
method := "GET"
var body io.Reader
if path == "/alerts.json" {
method = "POST"
payload, err := json.Marshal(models.AlertsRequest{
Filters: []string{},
GridLimits: map[string]int{},
DefaultGroupLimit: 50,
})
if err != nil {
t.Error(err)
t.FailNow()
}
body = bytes.NewReader(payload)
}
req := httptest.NewRequest(method, path, body)
for k, v := range testCase.requestHeaders {
req.Header.Set(k, v)
}
@@ -1307,6 +1470,10 @@ func TestAuthentication(t *testing.T) {
if ur.Authentication.Username != testCase.responseUsername {
t.Errorf("Got Authentication.Username=%s, expected %s", ur.Authentication.Username, testCase.responseUsername)
}
if diff := cmp.Diff(ur.Authentication.Groups, testCase.responseGroups); diff != "" {
t.Errorf("Incorrect groups list (-want +got):\n%s", diff)
break
}
}
}
})
+20 -2
View File
@@ -45,6 +45,9 @@ authentication:
header:
name: string
value_re: regex
group_name: string
group_value_re: regex
group_value_separator: string
basicAuth:
users:
- username: string
@@ -66,9 +69,9 @@ authentication:
- `authentication:users:header:group_value_re` - Similar to
`authentication:users:header:value_re`, but for groups instead of usernames.
Must be set when `authentication:users:header:group_name` is set.
- `authentication:users:header:group_value_separator` - If set, this will be
- `authentication:users:header:group_value_separator` - This will be
used to split the group header to multiple group names. The split is done
before evaluating the value regex. Optional.
after evaluating the value regex. Default value is `" "`.
- `authentication:users` - list of users (username & password) allowed to login.
Passwords are stored plain without any encryption.
When set HTTP basic authentication will be used.
@@ -107,6 +110,21 @@ authentication:
value_re: ^(.+)$
```
Example where the `X-Auth-User` and `X-Auth-Groups` headers will be used to
set username and list of groups. This assume that `X-Auth-Groups` value has
`Groups: foo,bar` syntax, where `foo` and `bar` are two groups user belongs to.
```YAML
authentication:
header:
name: X-Auth-User
value_re: ^(.+)$
group_name: X-Auth-Groups
group_value_re: 'Groups: (.+)'
group_value_separator: ','
```
### Authorization
`authorization` section allows to configure authorization groups used in
+15
View File
@@ -289,6 +289,10 @@ func (config *configSchema) Read(flags *pflag.FlagSet) (string, error) {
return "", fmt.Errorf("both authentication.basicAuth.users and authentication.header.name is set, only one can be enabled")
}
if config.Authentication.Header.GroupValueSeparator == "" {
config.Authentication.Header.GroupValueSeparator = " "
}
if config.Authentication.Header.ValueRegex != "" {
_, err = regex.CompileAnchored(config.Authentication.Header.ValueRegex)
if err != nil {
@@ -300,6 +304,17 @@ func (config *configSchema) Read(flags *pflag.FlagSet) (string, error) {
} else if config.Authentication.Header.Name != "" {
return "", fmt.Errorf("authentication.header.value_re is required when authentication.header.name is set")
}
if config.Authentication.Header.GroupValueRegex != "" {
_, err = regex.CompileAnchored(config.Authentication.Header.GroupValueRegex)
if err != nil {
return "", fmt.Errorf("invalid regex for authentication.header.group_value_re: %s", err.Error())
}
if config.Authentication.Header.GroupName == "" {
return "", fmt.Errorf("authentication.header.group_name is required when authentication.header.group_value_re is set")
}
} else if config.Authentication.Header.GroupName != "" {
return "", fmt.Errorf("authentication.header.group_value_re is required when authentication.header.group_name is set")
}
for _, u := range config.Authentication.BasicAuth.Users {
if u.Username == "" || u.Password == "" {
+3 -2
View File
@@ -425,8 +425,9 @@ type Settings struct {
}
type AuthenticationInfo struct {
Enabled bool `json:"enabled"`
Username string `json:"username"`
Enabled bool `json:"enabled"`
Username string `json:"username"`
Groups []string `json:"groups"`
}
type APIGrid struct {