diff --git a/README.md b/README.md index bbca3eb3..eb2a5909 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,8 @@ You can restrict who can login with the following parameters: Note, if you pass both `whitelist` and `domain`, then the default behaviour is for only `whitelist` to be used and `domain` will be effectively ignored. You can allow users matching *either* `whitelist` or `domain` by passing the `match-whitelist-or-domain` parameter (this will be the default behaviour in v3). +Domains and whitelists that are set in rules will always apply regardless of the `match-whitelist-or-domain` parameter + ### Forwarded Headers The authenticated user is set in the `X-Forwarded-User` header, to pass this on add this to the `authResponseHeaders` config option in traefik, as shown below in the [Applying Authentication](#applying-authentication) section. diff --git a/internal/auth.go b/internal/auth.go index cefd2315..647976e8 100644 --- a/internal/auth.go +++ b/internal/auth.go @@ -59,39 +59,57 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) { // ValidateEmail checks if the given email address matches either a whitelisted // email address, as defined by the "whitelist" config parameter. Or is part of // a permitted domain, as defined by the "domains" config parameter +// if an email or domain is added by a rule, it is merged with the config before being evaluated +// in order to respect the match whitelist or domain parameter func ValidateEmail(email string) bool { + // Do we have any validation to perform? - if len(config.Whitelist) == 0 && len(config.Domains) == 0 { + if len(config.Whitelist) == 0 && len(config.Domains) == 0 && len(config.Rules) == 0 { return true } + var whitelists []string + var domains []string - // Email whitelist validation - if len(config.Whitelist) > 0 { - for _, whitelist := range config.Whitelist { - if email == whitelist { - return true - } + // rule validation + for _, rule := range config.Rules { + for _, whitelisted := range rule.Whitelist { + whitelists = append(whitelists, whitelisted) + } + for _, domain := range rule.Domains { + domains = append(domains, domain) } + } + domains = append(domains, config.Domains...) + whitelists = append(whitelists, config.Whitelist...) - // If we're not matching *either*, stop here - if !config.MatchWhitelistOrDomain { - return false + // Email whitelist validation + for _, whitelist := range whitelists { + if email == whitelist { + return true } } + // If there's a whitelist and we're not matching *either*, stop here + if len(whitelists) > 0 && !config.MatchWhitelistOrDomain { + return false + } + // Domain validation - if len(config.Domains) > 0 { - parts := strings.Split(email, "@") - if len(parts) < 2 { - return false - } - for _, domain := range config.Domains { - if domain == parts[1] { - return true - } + for _, domain := range domains { + if emailIsInDomain(email, domain) { + return true } } - + return false +} +func emailIsInDomain(email string, domain string) bool { + parts := strings.Split(email, "@") + if len(parts) < 2 { + return false + } + if parts[1] == domain { + return true + } return false } diff --git a/internal/auth_test.go b/internal/auth_test.go index 840337c9..d4e3c8b0 100644 --- a/internal/auth_test.go +++ b/internal/auth_test.go @@ -72,6 +72,72 @@ func TestAuthValidateEmail(t *testing.T) { v = ValidateEmail("one@two.com") assert.True(v, "should allow any domain if email domain is not defined") + // Should allow email specified in a whitelist rule + config.Rules = map[string]*Rule{ + "whitelisted-from-rule": { + Rule: "test-whitelist-rule", + Whitelist: []string{"whitelisted-from-rule@example.com"}, + }, + } + + v = ValidateEmail("whitelisted-from-rule@example.com") + assert.True(v, "should allow email from whitelist rule") + + // Should allow multiple emails specified in a whitelist rule + config.Rules = map[string]*Rule{ + "whitelist-multiple-emails": { + Rule: "whitelist-rule", + Whitelist: []string{"allowed-from-whitelist-multiple-emails1@example.com", + "allowed-from-whitelist-multiple-emails2@example.com"}, + }, + } + + v1 := ValidateEmail("allowed-from-whitelist-multiple-emails@example.com") + v2 := ValidateEmail("allowed-from-whitelist-multiple-emails@example.com") + assert.True((v1 == v2), "should allow emails from a whitelist rule") + + // Should allow multiple whitelist rules + config.Rules = map[string]*Rule{ + "allowed-from-whitelist-rule1": { + Rule: "whitelist-rule1", + Whitelist: []string{"allowed-from-whitelist-rule1@example.com"}, + }, + "allowed-from-whitelist-rule2": { + Rule: "whitelist-rule2", + Whitelist: []string{"allowed-from-whitelist-rule2@example.com"}, + }, + } + v1 = ValidateEmail("allowed-from-whitelist-rule1@example.com") + v2 = ValidateEmail("allowed-from-whitelist-rule2@example.com") + assert.True((v1 == v2), "should allow emails from multiple whitelist rules") + + // Should allow domain to be specified in a rule + config.Rules = map[string]*Rule{ + "allowed-from-domain-rule": { + Rule: "domain-rule1", + Domains: []string{"example.com"}, + }, + } + + v = ValidateEmail("whitelisted-from-domain@example.com") + assert.True(v, "should allow email from domain rule") + + // Should allow multiple domains to be specified in a rule + config.Rules = map[string]*Rule{ + "allowed-from-domain-rule-1": { + Rule: "domain-rule1", + Domains: []string{"example.com"}, + }, + "allowed-from-domain-rule2": { + Rule: "domain-rule2", + Domains: []string{"example.org"}, + }, + } + + v1 = ValidateEmail("allowed-from-domain-rule1@example.org") + v2 = ValidateEmail("allowed-from-domain-rule2@example.com") + assert.True((v1 == v2), "Should allow emails from multiple domain rules") + // Should block non matching domain config.Domains = []string{"test.com"} v = ValidateEmail("one@two.com") @@ -99,26 +165,59 @@ func TestAuthValidateEmail(t *testing.T) { config.Domains = []string{"example.com"} config.Whitelist = []string{"test@test.com"} config.MatchWhitelistOrDomain = false + v = ValidateEmail("test@test.com") - assert.True(v, "should allow user in whitelist") + assert.True(v, "should allow user from whitelist config if MatchWhitelistOrDomain is disabled") + v = ValidateEmail("test@example.com") - assert.False(v, "should not allow user from valid domain") - v = ValidateEmail("one@two.com") - assert.False(v, "should not allow user not in either") + assert.False(v, "should not allow user from domain config if a whitelist is specified and MatchWhitelistOrDomain is disabled") - // Should allow either matching domain or email address when - // MatchWhitelistOrDomain is enabled - config.Domains = []string{"example.com"} - config.Whitelist = []string{"test@test.com"} + // Should allow only matching email address when + // MatchWhitelistOrDomain is disabled + config.Rules = map[string]*Rule{ + "allowed-from-whitelist-rule": { + Rule: "whitelist-rule", + Whitelist: []string{"allowed-from-whitelist-rule@example.com"}, + }, + "allowed-from-domain-rule": { + Rule: "domain-rule", + Domains: []string{"example.org"}, + }, + } + config.MatchWhitelistOrDomain = false + + v = ValidateEmail("allowed-from-whitelist-rule@example.com") + assert.True(v, "should allow user from whitelist rule if MatchWhitelistOrDomain is disabled") + + v = ValidateEmail("not-allowed-from-domain-rule@example.org") + assert.False(v, "should not allow user from domain rule if MatchWhitelistOrDomain is disabled") + + v = ValidateEmail("one@two.com") + assert.False(v, "should not allow user not in whitelist or domain") + + // Should allow matching email address or domain from a rule + // when MatchWhitelistOrDomain is enabled + config.Rules = map[string]*Rule{ + "allowed-from-whitelist-rule": { + Rule: "whitelist-rule", + Whitelist: []string{"allowed-from-whitelist-rule@example.com"}, + }, + "allowed-from-domain-rule": { + Rule: "domain-rule", + Domains: []string{"example.org"}, + }, + } config.MatchWhitelistOrDomain = true - v = ValidateEmail("test@test.com") - assert.True(v, "should allow user in whitelist") - v = ValidateEmail("test@example.com") - assert.True(v, "should allow user from valid domain") + + v = ValidateEmail("allowed-from-whitelist-rule@example.com") + assert.True(v, "should allow user from whitelist rule if MatchWhitelistOrDomain is true") + + v = ValidateEmail("not-allowed-from-domain-rule@example.org") + assert.True(v, "should allow user from domain rule if MatchWhitelistOrDomain is enabled") + v = ValidateEmail("one@two.com") - assert.False(v, "should not allow user not in either") + assert.False(v, "should not allow user not in whitelist or domain rule") } - func TestRedirectUri(t *testing.T) { assert := assert.New(t) diff --git a/internal/config.go b/internal/config.go index 8be0ae96..32d89038 100644 --- a/internal/config.go +++ b/internal/config.go @@ -210,6 +210,12 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [ rule.Rule = val case "provider": rule.Provider = val + case "whitelist": + list := CommaSeparatedList{} + rule.Whitelist = list + case "domains": + list := CommaSeparatedList{} + rule.Domains = list default: return args, fmt.Errorf("invalid route param: %v", option) } @@ -325,9 +331,11 @@ func (c *Config) setupProvider(name string) error { // Rule holds defined rules type Rule struct { - Action string - Rule string - Provider string + Action string + Rule string + Provider string + Whitelist []string + Domains []string } // NewRule creates a new rule object