From 7357417f48256698628e62998154705741011b24 Mon Sep 17 00:00:00 2001 From: Kent Rancourt Date: Thu, 23 Feb 2017 21:46:50 -0500 Subject: [PATCH] Allow usersFile to be specified for basic or digest auth --- docs/toml.md | 9 +++++- middlewares/authenticator.go | 46 +++++++++++++++++++++++---- middlewares/authenticator_test.go | 52 +++++++++++++++++++++++++++++++ traefik.sample.toml | 8 +++++ types/types.go | 6 ++-- 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/docs/toml.md b/docs/toml.md index 31ee4b409..53c9f82bc 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -191,20 +191,24 @@ Supported filters: # To enable basic auth on an entrypoint # 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 +# 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.http] # address = ":80" # [entryPoints.http.auth.basic] # users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +# usersFile = "/path/to/.htpasswd" # # To enable digest auth on an entrypoint # with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 # 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.http] # address = ":80" # [entryPoints.http.auth.basic] # 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): # [entryPoints] @@ -551,14 +555,17 @@ address = ":8080" # To enable basic auth on the webui # 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 +# 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] # users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +# usersFile = "/path/to/.htpasswd" # To enable digest auth on the webui # with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 # 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] # users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] - +# usersFile = "/path/to/.htdigest" ``` - `/`: provides a simple HTML frontend of Træfik diff --git a/middlewares/authenticator.go b/middlewares/authenticator.go index 2d4fb809b..f15b00342 100644 --- a/middlewares/authenticator.go +++ b/middlewares/authenticator.go @@ -2,6 +2,7 @@ package middlewares import ( "fmt" + "io/ioutil" "net/http" "strings" @@ -25,7 +26,7 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) { var err error authenticator := Authenticator{} if authConfig.Basic != nil { - authenticator.users, err = parserBasicUsers(authConfig.Basic.Users) + authenticator.users, err = parserBasicUsers(authConfig.Basic) if err != nil { return nil, err } @@ -43,7 +44,7 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) { } }) } else if authConfig.Digest != nil { - authenticator.users, err = parserDigestUsers(authConfig.Digest.Users) + authenticator.users, err = parserDigestUsers(authConfig.Digest) if err != nil { return nil, err } @@ -64,9 +65,17 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) { 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) - for _, user := range users { + for _, user := range userStrs { split := strings.Split(user, ":") if len(split) != 2 { 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 } -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) - for _, user := range users { + for _, user := range userStrs { split := strings.Split(user, ":") if len(split) != 3 { 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 } +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 { if secret, ok := a.users[user]; ok { return secret diff --git a/middlewares/authenticator_test.go b/middlewares/authenticator_test.go index f990f8681..cdcfc7b28 100644 --- a/middlewares/authenticator_test.go +++ b/middlewares/authenticator_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" "github.com/codegangsta/negroni" @@ -12,6 +13,57 @@ import ( "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) { authMiddleware, err := NewAuthenticator(&types.Auth{ Basic: &types.Basic{ diff --git a/traefik.sample.toml b/traefik.sample.toml index 9d7e2cd4c..0908c2ebc 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -247,20 +247,24 @@ # To enable basic auth on an entrypoint # 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 +# 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.http] # address = ":80" # [entryPoints.http.auth.basic] # users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +# usersFile = "/path/to/.htpasswd" # # To enable digest auth on an entrypoint # with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 # 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.http] # address = ":80" # [entryPoints.http.auth.basic] # 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): # [entryPoints] @@ -340,13 +344,17 @@ # To enable basic auth on the webui # 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 +# 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] # users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +# usersFile = "/path/to/.htpasswd" # To enable digest auth on the webui # with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 # 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] # users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] +# usersFile = "/path/to/.htdigest" ################################################################ diff --git a/types/types.go b/types/types.go index 308a71e25..815409ef7 100644 --- a/types/types.go +++ b/types/types.go @@ -241,12 +241,14 @@ type Users []string // Basic HTTP basic authentication type Basic struct { - Users `mapstructure:","` + Users `mapstructure:","` + UsersFile string } // Digest HTTP authentication type Digest struct { - Users `mapstructure:","` + Users `mapstructure:","` + UsersFile string } // CanonicalDomain returns a lower case domain with trim space