Merge pull request #1189 from krancour/usersfile
Allow usersFile to be specified for basic or digest auth
This commit is contained in:
commit
6e8e597ff5
5 changed files with 112 additions and 9 deletions
|
@ -191,20 +191,24 @@ Supported filters:
|
||||||
# To enable basic auth on an entrypoint
|
# To enable basic auth on an entrypoint
|
||||||
# with 2 user/pass: test:test and test2:test2
|
# with 2 user/pass: test:test and test2:test2
|
||||||
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [entryPoints]
|
# [entryPoints]
|
||||||
# [entryPoints.http]
|
# [entryPoints.http]
|
||||||
# address = ":80"
|
# address = ":80"
|
||||||
# [entryPoints.http.auth.basic]
|
# [entryPoints.http.auth.basic]
|
||||||
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
||||||
|
# usersFile = "/path/to/.htpasswd"
|
||||||
#
|
#
|
||||||
# To enable digest auth on an entrypoint
|
# To enable digest auth on an entrypoint
|
||||||
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
||||||
# You can use htdigest to generate those ones
|
# You can use htdigest to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [entryPoints]
|
# [entryPoints]
|
||||||
# [entryPoints.http]
|
# [entryPoints.http]
|
||||||
# address = ":80"
|
# address = ":80"
|
||||||
# [entryPoints.http.auth.basic]
|
# [entryPoints.http.auth.basic]
|
||||||
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
||||||
|
# usersFile = "/path/to/.htdigest"
|
||||||
#
|
#
|
||||||
# To specify an https entrypoint with a minimum TLS version, and specifying an array of cipher suites (from crypto/tls):
|
# To specify an https entrypoint with a minimum TLS version, and specifying an array of cipher suites (from crypto/tls):
|
||||||
# [entryPoints]
|
# [entryPoints]
|
||||||
|
@ -551,14 +555,17 @@ address = ":8080"
|
||||||
# To enable basic auth on the webui
|
# To enable basic auth on the webui
|
||||||
# with 2 user/pass: test:test and test2:test2
|
# with 2 user/pass: test:test and test2:test2
|
||||||
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [web.auth.basic]
|
# [web.auth.basic]
|
||||||
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
||||||
|
# usersFile = "/path/to/.htpasswd"
|
||||||
# To enable digest auth on the webui
|
# To enable digest auth on the webui
|
||||||
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
||||||
# You can use htdigest to generate those ones
|
# You can use htdigest to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [web.auth.digest]
|
# [web.auth.digest]
|
||||||
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
||||||
|
# usersFile = "/path/to/.htdigest"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/`: provides a simple HTML frontend of Træfik
|
- `/`: provides a simple HTML frontend of Træfik
|
||||||
|
|
|
@ -2,6 +2,7 @@ package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
|
||||||
var err error
|
var err error
|
||||||
authenticator := Authenticator{}
|
authenticator := Authenticator{}
|
||||||
if authConfig.Basic != nil {
|
if authConfig.Basic != nil {
|
||||||
authenticator.users, err = parserBasicUsers(authConfig.Basic.Users)
|
authenticator.users, err = parserBasicUsers(authConfig.Basic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if authConfig.Digest != nil {
|
} else if authConfig.Digest != nil {
|
||||||
authenticator.users, err = parserDigestUsers(authConfig.Digest.Users)
|
authenticator.users, err = parserDigestUsers(authConfig.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -64,9 +65,17 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
|
||||||
return &authenticator, nil
|
return &authenticator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parserBasicUsers(users types.Users) (map[string]string, error) {
|
func parserBasicUsers(basic *types.Basic) (map[string]string, error) {
|
||||||
|
var userStrs []string
|
||||||
|
if basic.UsersFile != "" {
|
||||||
|
var err error
|
||||||
|
if userStrs, err = getLinesFromFile(basic.UsersFile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userStrs = append(basic.Users, userStrs...)
|
||||||
userMap := make(map[string]string)
|
userMap := make(map[string]string)
|
||||||
for _, user := range users {
|
for _, user := range userStrs {
|
||||||
split := strings.Split(user, ":")
|
split := strings.Split(user, ":")
|
||||||
if len(split) != 2 {
|
if len(split) != 2 {
|
||||||
return nil, fmt.Errorf("Error parsing Authenticator user: %v", user)
|
return nil, fmt.Errorf("Error parsing Authenticator user: %v", user)
|
||||||
|
@ -76,9 +85,17 @@ func parserBasicUsers(users types.Users) (map[string]string, error) {
|
||||||
return userMap, nil
|
return userMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parserDigestUsers(users types.Users) (map[string]string, error) {
|
func parserDigestUsers(digest *types.Digest) (map[string]string, error) {
|
||||||
|
var userStrs []string
|
||||||
|
if digest.UsersFile != "" {
|
||||||
|
var err error
|
||||||
|
if userStrs, err = getLinesFromFile(digest.UsersFile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userStrs = append(digest.Users, userStrs...)
|
||||||
userMap := make(map[string]string)
|
userMap := make(map[string]string)
|
||||||
for _, user := range users {
|
for _, user := range userStrs {
|
||||||
split := strings.Split(user, ":")
|
split := strings.Split(user, ":")
|
||||||
if len(split) != 3 {
|
if len(split) != 3 {
|
||||||
return nil, fmt.Errorf("Error parsing Authenticator user: %v", user)
|
return nil, fmt.Errorf("Error parsing Authenticator user: %v", user)
|
||||||
|
@ -88,6 +105,23 @@ func parserDigestUsers(users types.Users) (map[string]string, error) {
|
||||||
return userMap, nil
|
return userMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLinesFromFile(filename string) ([]string, error) {
|
||||||
|
dat, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Trim lines and filter out blanks
|
||||||
|
rawLines := strings.Split(string(dat), "\n")
|
||||||
|
var filteredLines []string
|
||||||
|
for _, rawLine := range rawLines {
|
||||||
|
line := strings.TrimSpace(rawLine)
|
||||||
|
if line != "" {
|
||||||
|
filteredLines = append(filteredLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredLines, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Authenticator) secretBasic(user, realm string) string {
|
func (a *Authenticator) secretBasic(user, realm string) string {
|
||||||
if secret, ok := a.users[user]; ok {
|
if secret, ok := a.users[user]; ok {
|
||||||
return secret
|
return secret
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/codegangsta/negroni"
|
"github.com/codegangsta/negroni"
|
||||||
|
@ -12,6 +13,57 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAuthUsersFromFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
authType string
|
||||||
|
usersStr string
|
||||||
|
userKeys []string
|
||||||
|
parserFunc func(fileName string) (map[string]string, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
authType: "basic",
|
||||||
|
usersStr: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/\ntest2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0\n",
|
||||||
|
userKeys: []string{"test", "test2"},
|
||||||
|
parserFunc: func(fileName string) (map[string]string, error) {
|
||||||
|
basic := &types.Basic{
|
||||||
|
UsersFile: fileName,
|
||||||
|
}
|
||||||
|
return parserBasicUsers(basic)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authType: "digest",
|
||||||
|
usersStr: "test:traefik:a2688e031edb4be6a3797f3882655c05 \ntest2:traefik:518845800f9e2bfb1f1f740ec24f074e\n",
|
||||||
|
userKeys: []string{"test:traefik", "test2:traefik"},
|
||||||
|
parserFunc: func(fileName string) (map[string]string, error) {
|
||||||
|
digest := &types.Digest{
|
||||||
|
UsersFile: fileName,
|
||||||
|
}
|
||||||
|
return parserDigestUsers(digest)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.authType, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
usersFile, err := ioutil.TempFile("", "auth-users")
|
||||||
|
assert.NoError(t, err, "there should be no error")
|
||||||
|
defer os.Remove(usersFile.Name())
|
||||||
|
_, err = usersFile.Write([]byte(test.usersStr))
|
||||||
|
assert.NoError(t, err, "there should be no error")
|
||||||
|
users, err := test.parserFunc(usersFile.Name())
|
||||||
|
assert.NoError(t, err, "there should be no error")
|
||||||
|
assert.Equal(t, 2, len(users), "they should be equal")
|
||||||
|
_, ok := users[test.userKeys[0]]
|
||||||
|
assert.True(t, ok, "user test should be found")
|
||||||
|
_, ok = users[test.userKeys[1]]
|
||||||
|
assert.True(t, ok, "user test2 should be found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBasicAuthFail(t *testing.T) {
|
func TestBasicAuthFail(t *testing.T) {
|
||||||
authMiddleware, err := NewAuthenticator(&types.Auth{
|
authMiddleware, err := NewAuthenticator(&types.Auth{
|
||||||
Basic: &types.Basic{
|
Basic: &types.Basic{
|
||||||
|
|
|
@ -247,20 +247,24 @@
|
||||||
# To enable basic auth on an entrypoint
|
# To enable basic auth on an entrypoint
|
||||||
# with 2 user/pass: test:test and test2:test2
|
# with 2 user/pass: test:test and test2:test2
|
||||||
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [entryPoints]
|
# [entryPoints]
|
||||||
# [entryPoints.http]
|
# [entryPoints.http]
|
||||||
# address = ":80"
|
# address = ":80"
|
||||||
# [entryPoints.http.auth.basic]
|
# [entryPoints.http.auth.basic]
|
||||||
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
||||||
|
# usersFile = "/path/to/.htpasswd"
|
||||||
#
|
#
|
||||||
# To enable digest auth on an entrypoint
|
# To enable digest auth on an entrypoint
|
||||||
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
||||||
# You can use htdigest to generate those ones
|
# You can use htdigest to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [entryPoints]
|
# [entryPoints]
|
||||||
# [entryPoints.http]
|
# [entryPoints.http]
|
||||||
# address = ":80"
|
# address = ":80"
|
||||||
# [entryPoints.http.auth.basic]
|
# [entryPoints.http.auth.basic]
|
||||||
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
||||||
|
# usersFile = "/path/to/.htdigest"
|
||||||
#
|
#
|
||||||
# To specify an https entrypoint with a minimum TLS version, and specifying an array of cipher suites (from crypto/tls):
|
# To specify an https entrypoint with a minimum TLS version, and specifying an array of cipher suites (from crypto/tls):
|
||||||
# [entryPoints]
|
# [entryPoints]
|
||||||
|
@ -340,13 +344,17 @@
|
||||||
# To enable basic auth on the webui
|
# To enable basic auth on the webui
|
||||||
# with 2 user/pass: test:test and test2:test2
|
# with 2 user/pass: test:test and test2:test2
|
||||||
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [web.auth.basic]
|
# [web.auth.basic]
|
||||||
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
||||||
|
# usersFile = "/path/to/.htpasswd"
|
||||||
# To enable digest auth on the webui
|
# To enable digest auth on the webui
|
||||||
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2
|
||||||
# You can use htdigest to generate those ones
|
# You can use htdigest to generate those ones
|
||||||
|
# Users can be specified directly in the toml file, or indirectly by referencing an external file; if both are provided, the two are merged, with external file contents having precedence
|
||||||
# [web.auth.digest]
|
# [web.auth.digest]
|
||||||
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"]
|
||||||
|
# usersFile = "/path/to/.htdigest"
|
||||||
|
|
||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
|
|
|
@ -242,11 +242,13 @@ type Users []string
|
||||||
// Basic HTTP basic authentication
|
// Basic HTTP basic authentication
|
||||||
type Basic struct {
|
type Basic struct {
|
||||||
Users `mapstructure:","`
|
Users `mapstructure:","`
|
||||||
|
UsersFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Digest HTTP authentication
|
// Digest HTTP authentication
|
||||||
type Digest struct {
|
type Digest struct {
|
||||||
Users `mapstructure:","`
|
Users `mapstructure:","`
|
||||||
|
UsersFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanonicalDomain returns a lower case domain with trim space
|
// CanonicalDomain returns a lower case domain with trim space
|
||||||
|
|
Loading…
Reference in a new issue