Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow usersFile to be specified for basic or digest auth #1189

Merged
merged 1 commit into from
Mar 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
46 changes: 40 additions & 6 deletions middlewares/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package middlewares

import (
"fmt"
"io/ioutil"
"net/http"
"strings"

Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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 != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check basic for nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at where this function is invoked, basic is known not to be nil before this function is called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right.

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)
Expand All @@ -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 != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with parserBasicUsers() function, there's no need to check because this function is already only invoked under circumstances where the argument known not to be nil.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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)
Expand All @@ -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
Expand Down
52 changes: 52 additions & 0 deletions middlewares/authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,65 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/codegangsta/negroni"
"github.com/containous/traefik/types"
"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{
Expand Down
8 changes: 8 additions & 0 deletions traefik.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"


################################################################
Expand Down
6 changes: 4 additions & 2 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basic and Digest seem type identical. Any particular reason we're not using a single type only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of those types are introduced by this PR. They existed previously. Your question may be valid, but this PR probably isn't the right venue for revisiting a design decision not made by this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this case might be too heavy to justify in terms of the boy scout rule.

}

// CanonicalDomain returns a lower case domain with trim space
Expand Down