-
Notifications
You must be signed in to change notification settings - Fork 5.1k
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
Conversation
Possible someone can review this? I think it's a straightforward change. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Thanks @krancour :)
/cc @containous/traefik
docs/toml.md
Outdated
@@ -551,14 +551,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, or indirectly by referencing a file; if both are provided, the two are merged, with file contents having precedence |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By direct, you mean auth information embedded in the request?
Shouldn't the direct specifications have precedence, similar to CLI arguments usually taking precedence over file or environment variable configuration settings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By direct, you mean auth information embedded in the request?
I'm not sure what request you are referring to. This is configuration. The users can be specified directly in the toml (as is done currently) or (after this PR) in a file, which you reference in the toml (i.e. indirectly).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, I got confused.
Maybe it makes sense to distinguish the two file types more clearly in the documentation by extending the sentence in line 554 like (emphasis by me)
Users can be specified directly in the toml file, or indirectly by referencing an external file;
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. That seems like a reasonable change. Will do.
func parserBasicUsers(users types.Users) (map[string]string, error) { | ||
func parserBasicUsers(basic *types.Basic) (map[string]string, error) { | ||
var userStrs []string | ||
if basic.UsersFile != "" { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right.
func parserDigestUsers(users types.Users) (map[string]string, error) { | ||
func parserDigestUsers(digest *types.Digest) (map[string]string, error) { | ||
var userStrs []string | ||
if digest.UsersFile != "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check for nil
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
middlewares/authenticator.go
Outdated
userMap := make(map[string]string) | ||
for _, user := range users { | ||
for _, user := range userStrs { | ||
if user == "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the strings (lines from a file) that we get back from getUserStringsFromFile()
can legitimately contain blank lines. Those need to be discarded since there's no username / password information we can parse out of an empty string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Do we possibly also need to skip whitespace-only lines?
Also, do we test this case somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes... I will amend this to trim the line before checking if it's an empty string. Nice catch. As for the tests, there is already trailing blank line... the tests actually failed before the lines above were added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
middlewares/authenticator.go
Outdated
for _, user := range userStrs { | ||
if user == "" { | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reading logic is identical to the part above, right? Could we move it straight into getUserStringsFromFile
(maybe with adjusted function signature)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the question. getUserStringsFromFile()
just returns the a slice of strings where each string is a single line from the file. What follows the invocation of that function is logic for decomposing each of those strings into user and password (for basic auth) or user, password, and realm (for digest auth). The logic for those two is, necessarily, different. I'm not sure I can spot here what you are perceiving to be a missed opportunity to factor out more common code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first sight, lines 69 to 81 and 92 to 104 seem to match up quite well. On a second look, the reason why we seemingly cannot extract these parts into a common function (possibly getUserStringsFromFile()
) easily is because we use two different types (types.Basic
and types.Digest
) that are structurally equivalent. While I can't see why they have to be distinct, I admit it might be better to keep it this way for now (mostly because it's not clear to me what the struct field tags do exactly).
We could still extend getUserStringsFromFile()
to filter out empty strings. I'll leave the decision up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. And I agree that maybe two types aren't needed for basic and digest. That design decision should be revisited separately from this PR maybe? But for now... ya... I agree that the function that reads lines should probably filter out the blanks so that bit of logic doesn't have to be repeated in two other places. Good call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, let's leave the basic / digest type design decision to a different time and PR.
middlewares/authenticator_test.go
Outdated
assert.True(t, ok, "user test should be found") | ||
_, ok = users["test:traefik"] | ||
assert.True(t, ok, "user test2 should be found") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about turning the two tests into a single one using test tables? They look pretty identical except for a few parts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't a table-driven test make sense when the all the inputs (in a given column) are are of the same? Or when the function under test is the same for each row?
In these two tests, the fixtures (inputs) are different types and the functions under test are distinct. i.e. I humbly suggest the similarity between these two tests is on a superficial level and trying to combine these will not be trivial... if accomplished, it's certain to obfuscate what is otherwise some very simple, straightforward test logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, the decision to use a table-driven test or not is determined by the trade-off to be made between the amount of variance that needs to go into the test and the level of duplication you can remove. The two tests do seem to have quite a bit of code in common.
For the sake of comparability, here are the new tests in a table-driven fashion:
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")
})
}
}
For me, the only thing that tips this from a "pro" table-driven to a "maybe" is, again, the distinct authentication types. If those where the same, parserFunc
could be directly assigned to the respective function under test.
Personally, I prefer the table-driven approach in this case. I'm fine with sticking with two tests as well though if you believe that's better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into this further. Thanks for the example.
} | ||
|
||
// Digest HTTP authentication | ||
type Digest struct { | ||
Users `mapstructure:","` | ||
Users `mapstructure:","` | ||
UsersFile string |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
middlewares/authenticator.go
Outdated
@@ -88,6 +111,14 @@ func parserDigestUsers(users types.Users) (map[string]string, error) { | |||
return userMap, nil | |||
} | |||
|
|||
func getUserStringsFromFile(filename string) ([]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this function is just generically loading a file into memory and splitting the content by newlines, should we rather call it something like getLinesFromFile
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems reasonable... but should it be moved into some util package then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not aware that we have any (yet). Did a real quick look but couldn't find anything.
I'd say let's keep it in this package for now. Once we see similar call needs surfacing, we can extract into a new library package.
middlewares/authenticator_test.go
Outdated
|
||
func TestDigestAuthUsersFromFile(t *testing.T) { | ||
usersStr := "test:traefik:a2688e031edb4be6a3797f3882655c05 \ntest2:traefik:518845800f9e2bfb1f1f740ec24f074e\n" | ||
usersFile, err := ioutil.TempFile("", "basic-users") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the prefix be called "digest-users" for the sake of consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. Don't even know how I screwed that up. Good call. Will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found one small test bug in the second round. The rest is mostly debatable. :)
middlewares/authenticator_test.go
Outdated
assert.Equal(t, 2, len(users), "they should be equal") | ||
_, ok := users["test:traefik"] | ||
assert.True(t, ok, "user test should be found") | ||
_, ok = users["test:traefik"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be test2:traefik
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. Will fix. Thanks!
@timoreimann every actionable thing we discussed has been addressed now. Thank you for the help! Possible to re-review? |
@krancour Could you also complete |
middlewares/authenticator.go
Outdated
} | ||
// Trim lines and filter out blanks | ||
rawLines := strings.Split(string(dat), "\n") | ||
filteredLines := rawLines[:0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just creates an empty slice, correct? Any reason not to use a (IMHO) more clear var filteredLines []string
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read somewhere that I could bypass an allocation this way. It's allegedly marginally faster. If you want to take that small hit for the sake of clarity, I am not opposed. Just let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has something to do with the array backing the first slice being reused for the second slice... I'll be honest, it sounds suspicious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok, I think I get the performance point. Nevertheless, I'd say let's go with the clearer approach unless there's true reason to be believe we need to micro-optimize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@timoreimann, fixed.
@timoreimann back to a state of all actionable discussion points having been addressed. |
Looking very good! I think all that's left to do now is extend the sample file as mentioned by Emile. |
Yep... just realized I overlooked that comment. Will do. |
@emilevauge @timoreimann this has now been addressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Closes #1055