Allow override of domains and whitelist in rules (#169)

Co-authored-by: Mathieu Cantin <mcantin@petalmd.com>
Co-authored-by: Pete Shaw <lozlow@users.noreply.github.com>
This commit is contained in:
Thom Seddon 2020-09-23 14:50:15 +01:00 committed by GitHub
parent 41560feaa7
commit 04f5499f0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 48 deletions

View File

@ -321,6 +321,7 @@ All options can be supplied in any of the following ways, in the following prece
- `action` - same usage as [`default-action`](#default-action), supported values: - `action` - same usage as [`default-action`](#default-action), supported values:
- `auth` (default) - `auth` (default)
- `allow` - `allow`
- `domains` - optional, same usage as [`domain`](#domain)
- `provider` - same usage as [`default-provider`](#default-provider), supported values: - `provider` - same usage as [`default-provider`](#default-provider), supported values:
- `google` - `google`
- `oidc` - `oidc`
@ -333,6 +334,7 @@ All options can be supplied in any of the following ways, in the following prece
- ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)`` - ``Path(`path`, `/articles/{category}/{id:[0-9]+}`, ...)``
- ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)`` - ``PathPrefix(`/products/`, `/articles/{category}/{id:[0-9]+}`)``
- ``Query(`foo=bar`, `bar=baz`)`` - ``Query(`foo=bar`, `bar=baz`)``
- `whitelist` - optional, same usage as whitelist`](#whitelist)
For example: For example:
``` ```
@ -348,6 +350,11 @@ All options can be supplied in any of the following ways, in the following prece
rule.oidc.action = auth rule.oidc.action = auth
rule.oidc.provider = oidc rule.oidc.provider = oidc
rule.oidc.rule = PathPrefix(`/github`) rule.oidc.rule = PathPrefix(`/github`)
# Allow jane@example.com to `/janes-eyes-only`
rule.two.action = allow
rule.two.rule = Path(`/janes-eyes-only`)
rule.two.whitelist = jane@example.com
``` ```
Note: It is possible to break your redirect flow with rules, please be careful not to create an `allow` rule that matches your redirect_uri unless you know what you're doing. This limitation is being tracked in in #101 and the behaviour will change in future releases. Note: It is possible to break your redirect flow with rules, please be careful not to create an `allow` rule that matches your redirect_uri unless you know what you're doing. This limitation is being tracked in in #101 and the behaviour will change in future releases.
@ -361,7 +368,7 @@ You can restrict who can login with the following parameters:
* `domain` - Use this to limit logins to a specific domain, e.g. test.com only * `domain` - Use this to limit logins to a specific domain, e.g. test.com only
* `whitelist` - Use this to only allow specific users to login e.g. thom@test.com only * `whitelist` - Use this to only allow specific users to login e.g. thom@test.com only
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). 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). If you set `domains` or `whitelist` on a rule, the global configuration is ignored.
### Forwarded Headers ### Forwarded Headers

View File

@ -59,18 +59,28 @@ func ValidateCookie(r *http.Request, c *http.Cookie) (string, error) {
// ValidateEmail checks if the given email address matches either a whitelisted // ValidateEmail checks if the given email address matches either a whitelisted
// email address, as defined by the "whitelist" config parameter. Or is part of // email address, as defined by the "whitelist" config parameter. Or is part of
// a permitted domain, as defined by the "domains" config parameter // a permitted domain, as defined by the "domains" config parameter
func ValidateEmail(email string) bool { func ValidateEmail(email, ruleName string) bool {
// Use global config by default
whitelist := config.Whitelist
domains := config.Domains
if rule, ok := config.Rules[ruleName]; ok {
// Override with rule config if found
if len(rule.Whitelist) > 0 || len(rule.Domains) > 0 {
whitelist = rule.Whitelist
domains = rule.Domains
}
}
// Do we have any validation to perform? // Do we have any validation to perform?
if len(config.Whitelist) == 0 && len(config.Domains) == 0 { if len(whitelist) == 0 && len(domains) == 0 {
return true return true
} }
// Email whitelist validation // Email whitelist validation
if len(config.Whitelist) > 0 { if len(whitelist) > 0 {
for _, whitelist := range config.Whitelist { if ValidateWhitelist(email, whitelist) {
if email == whitelist { return true
return true
}
} }
// If we're not matching *either*, stop here // If we're not matching *either*, stop here
@ -80,21 +90,37 @@ func ValidateEmail(email string) bool {
} }
// Domain validation // Domain validation
if len(config.Domains) > 0 { if len(domains) > 0 && ValidateDomains(email, domains) {
parts := strings.Split(email, "@") return true
if len(parts) < 2 {
return false
}
for _, domain := range config.Domains {
if domain == parts[1] {
return true
}
}
} }
return false return false
} }
// ValidateWhitelist checks if the email is in whitelist
func ValidateWhitelist(email string, whitelist CommaSeparatedList) bool {
for _, whitelist := range whitelist {
if email == whitelist {
return true
}
}
return false
}
// ValidateDomains checks if the email matches a whitelisted domain
func ValidateDomains(email string, domains CommaSeparatedList) bool {
parts := strings.Split(email, "@")
if len(parts) < 2 {
return false
}
for _, domain := range domains {
if domain == parts[1] {
return true
}
}
return false
}
// Utility methods // Utility methods
// Get the redirect base // Get the redirect base

View File

@ -65,32 +65,25 @@ func TestAuthValidateEmail(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
config, _ = NewConfig([]string{}) config, _ = NewConfig([]string{})
// Should allow any // Should allow any with no whitelist/domain is specified
v := ValidateEmail("test@test.com") v := ValidateEmail("test@test.com", "default")
assert.True(v, "should allow any domain if email domain is not defined") assert.True(v, "should allow any domain if email domain is not defined")
v = ValidateEmail("one@two.com") v = ValidateEmail("one@two.com", "default")
assert.True(v, "should allow any domain if email domain is not defined") assert.True(v, "should allow any domain if email domain is not defined")
// Should block non matching domain
config.Domains = []string{"test.com"}
v = ValidateEmail("one@two.com")
assert.False(v, "should not allow user from another domain")
// Should allow matching domain // Should allow matching domain
config.Domains = []string{"test.com"} config.Domains = []string{"test.com"}
v = ValidateEmail("test@test.com") v = ValidateEmail("one@two.com", "default")
assert.False(v, "should not allow user from another domain")
v = ValidateEmail("test@test.com", "default")
assert.True(v, "should allow user from allowed domain") assert.True(v, "should allow user from allowed domain")
// Should block non whitelisted email address
config.Domains = []string{}
config.Whitelist = []string{"test@test.com"}
v = ValidateEmail("one@two.com")
assert.False(v, "should not allow user not in whitelist")
// Should allow matching whitelisted email address // Should allow matching whitelisted email address
config.Domains = []string{} config.Domains = []string{}
config.Whitelist = []string{"test@test.com"} config.Whitelist = []string{"test@test.com"}
v = ValidateEmail("test@test.com") v = ValidateEmail("one@two.com", "default")
assert.False(v, "should not allow user not in whitelist")
v = ValidateEmail("test@test.com", "default")
assert.True(v, "should allow user in whitelist") assert.True(v, "should allow user in whitelist")
// Should allow only matching email address when // Should allow only matching email address when
@ -98,24 +91,106 @@ func TestAuthValidateEmail(t *testing.T) {
config.Domains = []string{"example.com"} config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"} config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = false config.MatchWhitelistOrDomain = false
v = ValidateEmail("test@test.com") v = ValidateEmail("one@two.com", "default")
assert.True(v, "should allow user in whitelist")
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 not in either")
v = ValidateEmail("test@example.com", "default")
assert.False(v, "should not allow user from allowed domain")
v = ValidateEmail("test@test.com", "default")
assert.True(v, "should allow user in whitelist")
// Should allow either matching domain or email address when // Should allow either matching domain or email address when
// MatchWhitelistOrDomain is enabled // MatchWhitelistOrDomain is enabled
config.Domains = []string{"example.com"} config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"} config.Whitelist = []string{"test@test.com"}
config.MatchWhitelistOrDomain = true config.MatchWhitelistOrDomain = true
v = ValidateEmail("test@test.com") v = ValidateEmail("one@two.com", "default")
assert.True(v, "should allow user in whitelist")
v = ValidateEmail("test@example.com")
assert.True(v, "should 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 not in either")
v = ValidateEmail("test@example.com", "default")
assert.True(v, "should allow user from allowed domain")
v = ValidateEmail("test@test.com", "default")
assert.True(v, "should allow user in whitelist")
// Rule testing
// Should use global whitelist/domain when not specified on rule
config.Domains = []string{"example.com"}
config.Whitelist = []string{"test@test.com"}
config.Rules = map[string]*Rule{"test": NewRule()}
config.MatchWhitelistOrDomain = true
v = ValidateEmail("one@two.com", "test")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("test@example.com", "test")
assert.True(v, "should allow user from allowed global domain")
v = ValidateEmail("test@test.com", "test")
assert.True(v, "should allow user in global whitelist")
// Should allow matching domain in rule
config.Domains = []string{"testglobal.com"}
config.Whitelist = []string{}
rule := NewRule()
config.Rules = map[string]*Rule{"test": rule}
rule.Domains = []string{"testrule.com"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("one@two.com", "test")
assert.False(v, "should not allow user from another domain")
v = ValidateEmail("one@testglobal.com", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("test@testrule.com", "test")
assert.True(v, "should allow user from allowed domain")
// Should allow matching whitelist in rule
config.Domains = []string{}
config.Whitelist = []string{"test@testglobal.com"}
rule = NewRule()
config.Rules = map[string]*Rule{"test": rule}
rule.Whitelist = []string{"test@testrule.com"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("one@two.com", "test")
assert.False(v, "should not allow user from another domain")
v = ValidateEmail("test@testglobal.com", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("test@testrule.com", "test")
assert.True(v, "should allow user from allowed domain")
// Should allow only matching email address when
// MatchWhitelistOrDomain is disabled
config.Domains = []string{"exampleglobal.com"}
config.Whitelist = []string{"test@testglobal.com"}
rule = NewRule()
config.Rules = map[string]*Rule{"test": rule}
rule.Domains = []string{"examplerule.com"}
rule.Whitelist = []string{"test@testrule.com"}
config.MatchWhitelistOrDomain = false
v = ValidateEmail("one@two.com", "test")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("test@testglobal.com", "test")
assert.False(v, "should not allow user in global whitelist")
v = ValidateEmail("test@exampleglobal.com", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("test@examplerule.com", "test")
assert.False(v, "should not allow user from allowed domain")
v = ValidateEmail("test@testrule.com", "test")
assert.True(v, "should allow user in whitelist")
// Should allow either matching domain or email address when
// MatchWhitelistOrDomain is enabled
config.Domains = []string{"exampleglobal.com"}
config.Whitelist = []string{"test@testglobal.com"}
rule = NewRule()
config.Rules = map[string]*Rule{"test": rule}
rule.Domains = []string{"examplerule.com"}
rule.Whitelist = []string{"test@testrule.com"}
config.MatchWhitelistOrDomain = true
v = ValidateEmail("one@two.com", "test")
assert.False(v, "should not allow user not in either")
v = ValidateEmail("test@testglobal.com", "test")
assert.False(v, "should not allow user in global whitelist")
v = ValidateEmail("test@exampleglobal.com", "test")
assert.False(v, "should not allow user from global domain")
v = ValidateEmail("test@examplerule.com", "test")
assert.True(v, "should allow user from allowed domain")
v = ValidateEmail("test@testrule.com", "test")
assert.True(v, "should allow user in whitelist")
} }
func TestRedirectUri(t *testing.T) { func TestRedirectUri(t *testing.T) {

View File

@ -210,6 +210,14 @@ func (c *Config) parseUnknownFlag(option string, arg flags.SplitArgument, args [
rule.Rule = val rule.Rule = val
case "provider": case "provider":
rule.Provider = val rule.Provider = val
case "whitelist":
list := CommaSeparatedList{}
list.UnmarshalFlag(val)
rule.Whitelist = list
case "domains":
list := CommaSeparatedList{}
list.UnmarshalFlag(val)
rule.Domains = list
default: default:
return args, fmt.Errorf("invalid route param: %v", option) return args, fmt.Errorf("invalid route param: %v", option)
} }
@ -327,9 +335,11 @@ func (c *Config) setupProvider(name string) error {
// Rule holds defined rules // Rule holds defined rules
type Rule struct { type Rule struct {
Action string Action string
Rule string Rule string
Provider string Provider string
Whitelist CommaSeparatedList
Domains CommaSeparatedList
} }
// NewRule creates a new rule object // NewRule creates a new rule object

View File

@ -101,7 +101,7 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
} }
// Validate user // Validate user
valid := ValidateEmail(email) valid := ValidateEmail(email, rule)
if !valid { if !valid {
logger.WithField("email", email).Warn("Invalid email") logger.WithField("email", email).Warn("Invalid email")
http.Error(w, "Not authorized", 401) http.Error(w, "Not authorized", 401)