feat(auth): allow reading user groups from headers

Closes: #3361
Signed-off-by: Taavi Väänänen <hi@taavi.wtf>
This commit is contained in:
Taavi Väänänen
2021-09-28 16:07:18 +03:00
committed by Łukasz Mierzwa
parent bda2fcbc29
commit 9b89b4ee2a
13 changed files with 74 additions and 11 deletions

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"net/http"
"strings"
"github.com/prymitive/karma/internal/config"
"github.com/prymitive/karma/internal/regex"
@@ -21,7 +22,7 @@ func userGroups(username string) []string {
return groups
}
func headerAuth(name, valueRegex string, allowBypass []string) func(next http.Handler) http.Handler {
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) {
if slices.StringInSlice(allowBypass, r.URL.Path) {
@@ -38,16 +39,33 @@ func headerAuth(name, valueRegex string, allowBypass []string) func(next http.Ha
reg := regex.MustCompileAnchored(valueRegex)
matches := reg.FindAllStringSubmatch(user, 1)
if len(matches) > 0 && len(matches[0]) > 1 {
userName := matches[0][1]
ctx := context.WithValue(r.Context(), authUserKey("user"), userName)
ctx = context.WithValue(ctx, authUserKey("groups"), userGroups(userName))
next.ServeHTTP(w, r.WithContext(ctx))
if len(matches) == 0 || len(matches[0]) <= 1 {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Access denied\n"))
return
}
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Access denied\n"))
userName := matches[0][1]
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])
}
}
}
ctx := context.WithValue(r.Context(), authUserKey("user"), userName)
ctx = context.WithValue(ctx, authUserKey("groups"), groups)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

View File

@@ -118,7 +118,14 @@ func setupRouter(router *chi.Mux, historyPoller *historyPoller) {
}
if config.Config.Authentication.Header.Name != "" {
config.Config.Authentication.Enabled = true
router.Use(headerAuth(config.Config.Authentication.Header.Name, config.Config.Authentication.Header.ValueRegex, allowAuthBypass))
router.Use(headerAuth(
config.Config.Authentication.Header.Name,
config.Config.Authentication.Header.ValueRegex,
config.Config.Authentication.Header.GroupName,
config.Config.Authentication.Header.GroupValueRegex,
config.Config.Authentication.Header.GroupValueSeparator,
allowAuthBypass,
))
} else if len(config.Config.Authentication.BasicAuth.Users) > 0 {
config.Config.Authentication.Enabled = true
users := map[string]string{}

View File

@@ -83,6 +83,9 @@ level=info msg="authentication:"
level=info msg=" header:"
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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -11,6 +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=" basicAuth:"
level=info msg=" users:"
level=info msg=" - username: number"

View File

@@ -11,6 +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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -11,6 +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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -11,6 +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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -13,6 +13,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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -11,6 +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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -11,6 +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=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"

View File

@@ -61,6 +61,14 @@ authentication:
group will be used as the silence form author field.
All regexes are anchored.
This option must be set when `authentication:users:header:name` is set.
- `authentication:users:header:group_name` - name of the header that will
contain any groups the user has.
- `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
used to split the group header to multiple group names. The split is done
before evaluating the value regex. Optional.
- `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.

View File

@@ -26,6 +26,9 @@ func testReadConfig(t *testing.T) {
header:
name: ""
value_re: ""
group_name: ""
group_value_re: ""
group_value_separator: ""
basicAuth:
users: []
authorization:

View File

@@ -68,8 +68,11 @@ type configSchema struct {
Authentication struct {
Enabled bool `yaml:"-" koanf:"-"`
Header struct {
Name string
ValueRegex string `yaml:"value_re" koanf:"value_re"`
Name string
ValueRegex string `yaml:"value_re" koanf:"value_re"`
GroupName string `yaml:"group_name" koanf:"group_name"`
GroupValueRegex string `yaml:"group_value_re" koanf:"group_value_re"`
GroupValueSeparator string `yaml:"group_value_separator" koanf:"group_value_separator"`
}
BasicAuth struct {
Users []AuthenticationUser