feat(authorization): acl resource regex named groups (#3597)
This adds the named group functionality from domain_regex to the resource criteria.pull/3199/head^2
parent
19a543289b
commit
ab1d0c51d3
|
@ -186,18 +186,7 @@ strings. When it's a list of strings the rule matches when __any__ of the domain
|
||||||
When used in conjunction with [domain](#domain) the rule will match when either the [domain](#domain) or the
|
When used in conjunction with [domain](#domain) the rule will match when either the [domain](#domain) or the
|
||||||
[domain_regex](#domain_regex) criteria matches.
|
[domain_regex](#domain_regex) criteria matches.
|
||||||
|
|
||||||
This criteria takes any standard go regex pattern to match the requests. We additionally utilize two special named match
|
In addition to standard regex patterns this criteria can match some [Named Regex Groups](#named-regex-groups).
|
||||||
groups which match attributes of the user:
|
|
||||||
|
|
||||||
| Group Name | Match Value |
|
|
||||||
|:----------:|:-----------------:|
|
|
||||||
| User | username |
|
|
||||||
| Group | groups (contains) |
|
|
||||||
|
|
||||||
For the group match it matches if the user has any group name that matches, and both matches are case-insensitive due to
|
|
||||||
the fact domain names should not be compared in a case-sensitive way as per the
|
|
||||||
[RFC4343](https://www.rfc-editor.org/rfc/rfc4343.html) abstract and
|
|
||||||
[RFC3986 Section 3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2).
|
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
|
|
||||||
|
@ -395,6 +384,8 @@ strings. If any one of the regular expressions in the list matches the request i
|
||||||
for debugging these regular expressions is called [Regex 101](https://regex101.com/) (ensure you pick the `Golang`
|
for debugging these regular expressions is called [Regex 101](https://regex101.com/) (ensure you pick the `Golang`
|
||||||
option).
|
option).
|
||||||
|
|
||||||
|
In addition to standard regex patterns this criteria can match some [Named Regex Groups](#named-regex-groups).
|
||||||
|
|
||||||
*__Note:__ Prior to 4.27.0 the regular expressions only matched the path excluding the query parameters. After 4.27.0
|
*__Note:__ Prior to 4.27.0 the regular expressions only matched the path excluding the query parameters. After 4.27.0
|
||||||
they match the entire path including the query parameters. When upgrading you may be required to alter some of your
|
they match the entire path including the query parameters. When upgrading you may be required to alter some of your
|
||||||
resource rules to get them to operate as they previously did.*
|
resource rules to get them to operate as they previously did.*
|
||||||
|
@ -456,6 +447,20 @@ performed 2FA then they will be allowed to access the resource.
|
||||||
This policy requires the user to complete 2FA successfully. This is currently the highest level of authentication
|
This policy requires the user to complete 2FA successfully. This is currently the highest level of authentication
|
||||||
policy available.
|
policy available.
|
||||||
|
|
||||||
|
## Named Regex Groups
|
||||||
|
|
||||||
|
Some criteria allow matching named regex groups. These are the groups we accept:
|
||||||
|
|
||||||
|
| Group Name | Match Value |
|
||||||
|
|:----------:|:-----------------:|
|
||||||
|
| User | username |
|
||||||
|
| Group | groups (contains) |
|
||||||
|
|
||||||
|
For the group name `Group` the regex pattern matches if the user has the specific group name matching the pattern. Both
|
||||||
|
regex groups are case-insensitive due to the fact that the regex groups are used in domain criteria and domain names
|
||||||
|
should not be compared in a case-sensitive way as per the [RFC4343](https://www.rfc-editor.org/rfc/rfc4343.html)
|
||||||
|
abstract and [RFC3986 Section 3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2).
|
||||||
|
|
||||||
## Detailed example
|
## Detailed example
|
||||||
|
|
||||||
Here is a detailed example of an example access control section:
|
Here is a detailed example of an example access control section:
|
||||||
|
|
|
@ -9,60 +9,30 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAccessControlDomain creates a new SubjectObjectMatcher that matches the domain as a basic string.
|
// NewAccessControlDomain creates a new SubjectObjectMatcher that matches the domain as a basic string.
|
||||||
func NewAccessControlDomain(domain string) SubjectObjectMatcher {
|
func NewAccessControlDomain(domain string) AccessControlDomain {
|
||||||
d := AccessControlDomain{}
|
m := &AccessControlDomainMatcher{}
|
||||||
|
|
||||||
domain = strings.ToLower(domain)
|
domain = strings.ToLower(domain)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(domain, "*."):
|
case strings.HasPrefix(domain, "*."):
|
||||||
d.Wildcard = true
|
m.Wildcard = true
|
||||||
d.Name = domain[1:]
|
m.Name = domain[1:]
|
||||||
case strings.HasPrefix(domain, "{user}"):
|
case strings.HasPrefix(domain, "{user}"):
|
||||||
d.UserWildcard = true
|
m.UserWildcard = true
|
||||||
d.Name = domain[7:]
|
m.Name = domain[7:]
|
||||||
case strings.HasPrefix(domain, "{group}"):
|
case strings.HasPrefix(domain, "{group}"):
|
||||||
d.GroupWildcard = true
|
m.GroupWildcard = true
|
||||||
d.Name = domain[8:]
|
m.Name = domain[8:]
|
||||||
default:
|
default:
|
||||||
d.Name = domain
|
m.Name = domain
|
||||||
}
|
}
|
||||||
|
|
||||||
return d
|
return AccessControlDomain{m}
|
||||||
}
|
|
||||||
|
|
||||||
// AccessControlDomain represents an ACL domain.
|
|
||||||
type AccessControlDomain struct {
|
|
||||||
Name string
|
|
||||||
Wildcard bool
|
|
||||||
UserWildcard bool
|
|
||||||
GroupWildcard bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMatch returns true if the ACL domain matches the object domain.
|
|
||||||
func (acl AccessControlDomain) IsMatch(subject Subject, object Object) (match bool) {
|
|
||||||
switch {
|
|
||||||
case acl.Wildcard:
|
|
||||||
return strings.HasSuffix(object.Domain, acl.Name)
|
|
||||||
case acl.UserWildcard:
|
|
||||||
return object.Domain == fmt.Sprintf("%s.%s", subject.Username, acl.Name)
|
|
||||||
case acl.GroupWildcard:
|
|
||||||
prefix, suffix := domainToPrefixSuffix(object.Domain)
|
|
||||||
|
|
||||||
return suffix == acl.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
|
||||||
default:
|
|
||||||
return object.Domain == acl.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of the SubjectObjectMatcher rule.
|
|
||||||
func (acl AccessControlDomain) String() string {
|
|
||||||
return fmt.Sprintf("domain:%s", acl.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAccessControlDomainRegex creates a new SubjectObjectMatcher that matches the domain either in a basic way or
|
// NewAccessControlDomainRegex creates a new SubjectObjectMatcher that matches the domain either in a basic way or
|
||||||
// dynamic User/Group subexpression group way.
|
// dynamic User/Group subexpression group way.
|
||||||
func NewAccessControlDomainRegex(pattern regexp.Regexp) SubjectObjectMatcher {
|
func NewAccessControlDomainRegex(pattern regexp.Regexp) AccessControlDomain {
|
||||||
var iuser, igroup = -1, -1
|
var iuser, igroup = -1, -1
|
||||||
|
|
||||||
for i, group := range pattern.SubexpNames() {
|
for i, group := range pattern.SubexpNames() {
|
||||||
|
@ -75,53 +45,42 @@ func NewAccessControlDomainRegex(pattern regexp.Regexp) SubjectObjectMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
if iuser != -1 || igroup != -1 {
|
if iuser != -1 || igroup != -1 {
|
||||||
return AccessControlDomainRegex{Pattern: pattern, SubexpNameUser: iuser, SubexpNameGroup: igroup}
|
return AccessControlDomain{RegexpGroupStringSubjectMatcher{pattern, iuser, igroup}}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AccessControlDomainRegexBasic{Pattern: pattern}
|
return AccessControlDomain{RegexpStringSubjectMatcher{pattern}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessControlDomainRegexBasic represents a basic domain regex SubjectObjectMatcher.
|
// AccessControlDomainMatcher is the basic domain matcher.
|
||||||
type AccessControlDomainRegexBasic struct {
|
type AccessControlDomainMatcher struct {
|
||||||
Pattern regexp.Regexp
|
Name string
|
||||||
|
Wildcard bool
|
||||||
|
UserWildcard bool
|
||||||
|
GroupWildcard bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMatch returns true if the ACL regex matches the object domain.
|
// IsMatch returns true if this rule matches.
|
||||||
func (acl AccessControlDomainRegexBasic) IsMatch(_ Subject, object Object) (match bool) {
|
func (m AccessControlDomainMatcher) IsMatch(domain string, subject Subject) (match bool) {
|
||||||
return acl.Pattern.MatchString(object.Domain)
|
switch {
|
||||||
}
|
case m.Wildcard:
|
||||||
|
return strings.HasSuffix(domain, m.Name)
|
||||||
|
case m.UserWildcard:
|
||||||
|
return domain == fmt.Sprintf("%s.%s", subject.Username, m.Name)
|
||||||
|
case m.GroupWildcard:
|
||||||
|
prefix, suffix := domainToPrefixSuffix(domain)
|
||||||
|
|
||||||
// String returns a text representation of a AccessControlDomainRegexBasic.
|
return suffix == m.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
||||||
func (acl AccessControlDomainRegexBasic) String() string {
|
default:
|
||||||
return fmt.Sprintf("domain_regex:%s", acl.Pattern.String())
|
return strings.EqualFold(domain, m.Name)
|
||||||
}
|
|
||||||
|
|
||||||
// AccessControlDomainRegex represents an ACL domain regex.
|
|
||||||
type AccessControlDomainRegex struct {
|
|
||||||
Pattern regexp.Regexp
|
|
||||||
SubexpNameUser int
|
|
||||||
SubexpNameGroup int
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMatch returns true if the ACL regex matches the object domain.
|
|
||||||
func (acl AccessControlDomainRegex) IsMatch(subject Subject, object Object) (match bool) {
|
|
||||||
matches := acl.Pattern.FindAllStringSubmatch(object.Domain, -1)
|
|
||||||
if matches == nil {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl.SubexpNameUser != -1 && !strings.EqualFold(subject.Username, matches[0][acl.SubexpNameUser]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if acl.SubexpNameGroup != -1 && !utils.IsStringInSliceFold(matches[0][acl.SubexpNameGroup], subject.Groups) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a text representation of a AccessControlDomainRegex.
|
// AccessControlDomain represents an ACL domain.
|
||||||
func (acl AccessControlDomainRegex) String() string {
|
type AccessControlDomain struct {
|
||||||
return fmt.Sprintf("domain_regex(subexp):%s", acl.Pattern.String())
|
Matcher StringSubjectMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if the ACL domain matches the object domain.
|
||||||
|
func (acl AccessControlDomain) IsMatch(subject Subject, object Object) (match bool) {
|
||||||
|
return acl.Matcher.IsMatch(object.Domain, subject)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,32 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccessControlResource represents an ACL resource.
|
// NewAccessControlResource creates a AccessControlResource or AccessControlResourceGroup.
|
||||||
|
func NewAccessControlResource(pattern regexp.Regexp) AccessControlResource {
|
||||||
|
var iuser, igroup = -1, -1
|
||||||
|
|
||||||
|
for i, group := range pattern.SubexpNames() {
|
||||||
|
switch group {
|
||||||
|
case subexpNameUser:
|
||||||
|
iuser = i
|
||||||
|
case subexpNameGroup:
|
||||||
|
igroup = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iuser != -1 || igroup != -1 {
|
||||||
|
return AccessControlResource{RegexpGroupStringSubjectMatcher{pattern, iuser, igroup}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AccessControlResource{RegexpStringSubjectMatcher{pattern}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessControlResource represents an ACL resource that matches without named groups.
|
||||||
type AccessControlResource struct {
|
type AccessControlResource struct {
|
||||||
Pattern regexp.Regexp
|
Matcher StringSubjectMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMatch returns true if the ACL resource match the object path.
|
// IsMatch returns true if the ACL resource match the object path.
|
||||||
func (acr AccessControlResource) IsMatch(object Object) (match bool) {
|
func (acl AccessControlResource) IsMatch(subject Subject, object Object) (match bool) {
|
||||||
return acr.Pattern.MatchString(object.Path)
|
return acl.Matcher.IsMatch(object.Path, subject)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ func NewAccessControlRule(pos int, rule schema.ACLRule, networksMap map[string][
|
||||||
// AccessControlRule controls and represents an ACL internally.
|
// AccessControlRule controls and represents an ACL internally.
|
||||||
type AccessControlRule struct {
|
type AccessControlRule struct {
|
||||||
Position int
|
Position int
|
||||||
Domains []SubjectObjectMatcher
|
Domains []AccessControlDomain
|
||||||
Resources []AccessControlResource
|
Resources []AccessControlResource
|
||||||
Methods []string
|
Methods []string
|
||||||
Networks []*net.IPNet
|
Networks []*net.IPNet
|
||||||
|
@ -48,7 +48,7 @@ func (acr *AccessControlRule) IsMatch(subject Subject, object Object) (match boo
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isMatchForResources(object, acr) {
|
if !isMatchForResources(subject, object, acr) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ func isMatchForDomains(subject Subject, object Object, acl *AccessControlRule) (
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMatchForResources(object Object, acl *AccessControlRule) (match bool) {
|
func isMatchForResources(subject Subject, object Object, acl *AccessControlRule) (match bool) {
|
||||||
// If there are no resources in this rule then the resource condition is a match.
|
// If there are no resources in this rule then the resource condition is a match.
|
||||||
if len(acl.Resources) == 0 {
|
if len(acl.Resources) == 0 {
|
||||||
return true
|
return true
|
||||||
|
@ -91,7 +91,7 @@ func isMatchForResources(object Object, acl *AccessControlRule) (match bool) {
|
||||||
|
|
||||||
// Iterate over the resources until we find a match (return true) or until we exit the loop (return false).
|
// Iterate over the resources until we find a match (return true) or until we exit the loop (return false).
|
||||||
for _, resource := range acl.Resources {
|
for _, resource := range acl.Resources {
|
||||||
if resource.IsMatch(object) {
|
if resource.IsMatch(subject, object) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ func (p Authorizer) GetRuleMatchResults(subject Subject, object Object) (results
|
||||||
Skipped: skipped,
|
Skipped: skipped,
|
||||||
|
|
||||||
MatchDomain: isMatchForDomains(subject, object, rule),
|
MatchDomain: isMatchForDomains(subject, object, rule),
|
||||||
MatchResources: isMatchForResources(object, rule),
|
MatchResources: isMatchForResources(subject, object, rule),
|
||||||
MatchMethods: isMatchForMethods(object, rule),
|
MatchMethods: isMatchForMethods(object, rule),
|
||||||
MatchNetworks: isMatchForNetworks(subject, rule),
|
MatchNetworks: isMatchForNetworks(subject, rule),
|
||||||
MatchSubjects: isMatchForSubjects(subject, rule),
|
MatchSubjects: isMatchForSubjects(subject, rule),
|
||||||
|
|
|
@ -231,7 +231,7 @@ func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
|
||||||
tester.CheckAuthorizations(s.T(), John, "https://public.example.com/", "GET", TwoFactor)
|
tester.CheckAuthorizations(s.T(), John, "https://public.example.com/", "GET", TwoFactor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestShouldcheckDomainMatching() {
|
func (s *AuthorizerSuite) TestShouldCheckDomainMatching() {
|
||||||
tester := NewAuthorizerBuilder().
|
tester := NewAuthorizerBuilder().
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"public.example.com"},
|
Domains: []string{"public.example.com"},
|
||||||
|
@ -272,20 +272,62 @@ func (s *AuthorizerSuite) TestShouldcheckDomainMatching() {
|
||||||
tester.CheckAuthorizations(s.T(), Bob, "https://x.example.com", "GET", TwoFactor)
|
tester.CheckAuthorizations(s.T(), Bob, "https://x.example.com", "GET", TwoFactor)
|
||||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://x.example.com", "GET", OneFactor)
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://x.example.com", "GET", OneFactor)
|
||||||
|
|
||||||
assert.Equal(s.T(), "public.example.com", tester.configuration.AccessControl.Rules[0].Domains[0])
|
s.Require().Len(tester.rules, 5)
|
||||||
assert.Equal(s.T(), "domain:public.example.com", tester.rules[0].Domains[0].String())
|
|
||||||
|
|
||||||
assert.Equal(s.T(), "one-factor.example.com", tester.configuration.AccessControl.Rules[1].Domains[0])
|
s.Require().Len(tester.rules[0].Domains, 1)
|
||||||
assert.Equal(s.T(), "domain:one-factor.example.com", tester.rules[1].Domains[0].String())
|
|
||||||
|
|
||||||
assert.Equal(s.T(), "two-factor.example.com", tester.configuration.AccessControl.Rules[2].Domains[0])
|
s.Assert().Equal("public.example.com", tester.configuration.AccessControl.Rules[0].Domains[0])
|
||||||
assert.Equal(s.T(), "domain:two-factor.example.com", tester.rules[2].Domains[0].String())
|
|
||||||
|
|
||||||
assert.Equal(s.T(), "*.example.com", tester.configuration.AccessControl.Rules[3].Domains[0])
|
ruleMatcher0, ok := tester.rules[0].Domains[0].Matcher.(*AccessControlDomainMatcher)
|
||||||
assert.Equal(s.T(), "domain:.example.com", tester.rules[3].Domains[0].String())
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("public.example.com", ruleMatcher0.Name)
|
||||||
|
s.Assert().False(ruleMatcher0.Wildcard)
|
||||||
|
s.Assert().False(ruleMatcher0.UserWildcard)
|
||||||
|
s.Assert().False(ruleMatcher0.GroupWildcard)
|
||||||
|
|
||||||
assert.Equal(s.T(), "*.example.com", tester.configuration.AccessControl.Rules[4].Domains[0])
|
s.Require().Len(tester.rules[1].Domains, 1)
|
||||||
assert.Equal(s.T(), "domain:.example.com", tester.rules[4].Domains[0].String())
|
|
||||||
|
s.Assert().Equal("one-factor.example.com", tester.configuration.AccessControl.Rules[1].Domains[0])
|
||||||
|
|
||||||
|
ruleMatcher1, ok := tester.rules[1].Domains[0].Matcher.(*AccessControlDomainMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("one-factor.example.com", ruleMatcher1.Name)
|
||||||
|
s.Assert().False(ruleMatcher1.Wildcard)
|
||||||
|
s.Assert().False(ruleMatcher1.UserWildcard)
|
||||||
|
s.Assert().False(ruleMatcher1.GroupWildcard)
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[2].Domains, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal("two-factor.example.com", tester.configuration.AccessControl.Rules[2].Domains[0])
|
||||||
|
|
||||||
|
ruleMatcher2, ok := tester.rules[2].Domains[0].Matcher.(*AccessControlDomainMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("two-factor.example.com", ruleMatcher2.Name)
|
||||||
|
s.Assert().False(ruleMatcher2.Wildcard)
|
||||||
|
s.Assert().False(ruleMatcher2.UserWildcard)
|
||||||
|
s.Assert().False(ruleMatcher2.GroupWildcard)
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[3].Domains, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal("*.example.com", tester.configuration.AccessControl.Rules[3].Domains[0])
|
||||||
|
|
||||||
|
ruleMatcher3, ok := tester.rules[3].Domains[0].Matcher.(*AccessControlDomainMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal(".example.com", ruleMatcher3.Name)
|
||||||
|
s.Assert().True(ruleMatcher3.Wildcard)
|
||||||
|
s.Assert().False(ruleMatcher3.UserWildcard)
|
||||||
|
s.Assert().False(ruleMatcher3.GroupWildcard)
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[4].Domains, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal("*.example.com", tester.configuration.AccessControl.Rules[4].Domains[0])
|
||||||
|
|
||||||
|
ruleMatcher4, ok := tester.rules[4].Domains[0].Matcher.(*AccessControlDomainMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal(".example.com", ruleMatcher4.Name)
|
||||||
|
s.Assert().True(ruleMatcher4.Wildcard)
|
||||||
|
s.Assert().False(ruleMatcher4.UserWildcard)
|
||||||
|
s.Assert().False(ruleMatcher4.GroupWildcard)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestShouldCheckDomainRegexMatching() {
|
func (s *AuthorizerSuite) TestShouldCheckDomainRegexMatching() {
|
||||||
|
@ -327,20 +369,135 @@ func (s *AuthorizerSuite) TestShouldCheckDomainRegexMatching() {
|
||||||
tester.CheckAuthorizations(s.T(), John, "https://group-dev.regex.com", "GET", TwoFactor)
|
tester.CheckAuthorizations(s.T(), John, "https://group-dev.regex.com", "GET", TwoFactor)
|
||||||
tester.CheckAuthorizations(s.T(), Bob, "https://group-dev.regex.com", "GET", Denied)
|
tester.CheckAuthorizations(s.T(), Bob, "https://group-dev.regex.com", "GET", Denied)
|
||||||
|
|
||||||
assert.Equal(s.T(), "^.*\\.example.com$", tester.configuration.AccessControl.Rules[0].DomainsRegex[0].String())
|
s.Require().Len(tester.rules, 5)
|
||||||
assert.Equal(s.T(), "domain_regex:^.*\\.example.com$", tester.rules[0].Domains[0].String())
|
|
||||||
|
|
||||||
assert.Equal(s.T(), "^.*\\.example2.com$", tester.configuration.AccessControl.Rules[1].DomainsRegex[0].String())
|
s.Require().Len(tester.rules[0].Domains, 1)
|
||||||
assert.Equal(s.T(), "domain_regex:^.*\\.example2.com$", tester.rules[1].Domains[0].String())
|
|
||||||
|
|
||||||
assert.Equal(s.T(), "^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[2].DomainsRegex[0].String())
|
s.Assert().Equal("^.*\\.example.com$", tester.configuration.AccessControl.Rules[0].DomainsRegex[0].String())
|
||||||
assert.Equal(s.T(), "domain_regex(subexp):^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.rules[2].Domains[0].String())
|
|
||||||
|
|
||||||
assert.Equal(s.T(), "^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[3].DomainsRegex[0].String())
|
ruleMatcher0, ok := tester.rules[0].Domains[0].Matcher.(RegexpStringSubjectMatcher)
|
||||||
assert.Equal(s.T(), "domain_regex(subexp):^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.rules[3].Domains[0].String())
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^.*\\.example.com$", ruleMatcher0.String())
|
||||||
|
|
||||||
assert.Equal(s.T(), "^.*\\.(one|two).com$", tester.configuration.AccessControl.Rules[4].DomainsRegex[0].String())
|
s.Require().Len(tester.rules[1].Domains, 1)
|
||||||
assert.Equal(s.T(), "domain_regex:^.*\\.(one|two).com$", tester.rules[4].Domains[0].String())
|
|
||||||
|
s.Assert().Equal("^.*\\.example2.com$", tester.configuration.AccessControl.Rules[1].DomainsRegex[0].String())
|
||||||
|
|
||||||
|
ruleMatcher1, ok := tester.rules[1].Domains[0].Matcher.(RegexpStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^.*\\.example2.com$", ruleMatcher1.String())
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[2].Domains, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal("^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[2].DomainsRegex[0].String())
|
||||||
|
|
||||||
|
ruleMatcher2, ok := tester.rules[2].Domains[0].Matcher.(RegexpGroupStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", ruleMatcher2.String())
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[3].Domains, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal("^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[3].DomainsRegex[0].String())
|
||||||
|
|
||||||
|
ruleMatcher3, ok := tester.rules[3].Domains[0].Matcher.(RegexpGroupStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", ruleMatcher3.String())
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[4].Domains, 1)
|
||||||
|
|
||||||
|
s.Assert().Equal("^.*\\.(one|two).com$", tester.configuration.AccessControl.Rules[4].DomainsRegex[0].String())
|
||||||
|
|
||||||
|
ruleMatcher4, ok := tester.rules[4].Domains[0].Matcher.(RegexpStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^.*\\.(one|two).com$", ruleMatcher4.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorizerSuite) TestShouldCheckResourceSubjectMatching() {
|
||||||
|
createSliceRegexRule := func(t *testing.T, rules []string) []regexp.Regexp {
|
||||||
|
result, err := stringSliceToRegexpSlice(rules)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
tester := NewAuthorizerBuilder().
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"id.example.com"},
|
||||||
|
Policy: oneFactor,
|
||||||
|
Resources: createSliceRegexRule(s.T(), []string{`^/(?P<User>[a-zA-Z0-9]+)/personal(/|/.*)?$`, `^/(?P<Group>[a-zA-Z0-9]+)/group(/|/.*)?$`}),
|
||||||
|
}).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"id.example.com"},
|
||||||
|
Policy: deny,
|
||||||
|
Resources: createSliceRegexRule(s.T(), []string{`^/([a-zA-Z0-9]+)/personal(/|/.*)?$`, `^/([a-zA-Z0-9]+)/group(/|/.*)?$`}),
|
||||||
|
}).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"id.example.com"},
|
||||||
|
Policy: bypass,
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
// Accessing the unprotected root.
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com", "GET", Bypass)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com", "GET", Bypass)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com", "GET", Bypass)
|
||||||
|
|
||||||
|
// Accessing Personal page.
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/john/personal", "GET", OneFactor)
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/John/personal", "GET", OneFactor)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/bob/personal", "GET", OneFactor)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/Bob/personal", "GET", OneFactor)
|
||||||
|
|
||||||
|
// Accessing an invalid users Personal page.
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/invaliduser/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/invaliduser/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/invaliduser/personal", "GET", Denied)
|
||||||
|
|
||||||
|
// Accessing another users Personal page.
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/bob/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/bob/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/Bob/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/Bob/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/john/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/john/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/John/personal", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/John/personal", "GET", Denied)
|
||||||
|
|
||||||
|
// Accessing a Group page.
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/dev/group", "GET", OneFactor)
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/admins/group", "GET", OneFactor)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/dev/group", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/admins/group", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/dev/group", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/admins/group", "GET", Denied)
|
||||||
|
|
||||||
|
// Accessing an invalid group's Group page.
|
||||||
|
tester.CheckAuthorizations(s.T(), John, "https://id.example.com/invalidgroup/group", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), Bob, "https://id.example.com/invalidgroup/group", "GET", Denied)
|
||||||
|
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://id.example.com/invalidgroup/group", "GET", Denied)
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules, 3)
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[0].Resources, 2)
|
||||||
|
|
||||||
|
ruleMatcher00, ok := tester.rules[0].Resources[0].Matcher.(RegexpGroupStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^/(?P<User>[a-zA-Z0-9]+)/personal(/|/.*)?$", ruleMatcher00.String())
|
||||||
|
|
||||||
|
ruleMatcher01, ok := tester.rules[0].Resources[1].Matcher.(RegexpGroupStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^/(?P<Group>[a-zA-Z0-9]+)/group(/|/.*)?$", ruleMatcher01.String())
|
||||||
|
|
||||||
|
s.Require().Len(tester.rules[1].Resources, 2)
|
||||||
|
|
||||||
|
ruleMatcher10, ok := tester.rules[1].Resources[0].Matcher.(RegexpStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^/([a-zA-Z0-9]+)/personal(/|/.*)?$", ruleMatcher10.String())
|
||||||
|
|
||||||
|
ruleMatcher11, ok := tester.rules[1].Resources[1].Matcher.(RegexpStringSubjectMatcher)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Assert().Equal("^/([a-zA-Z0-9]+)/group(/|/.*)?$", ruleMatcher11.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestShouldCheckUserMatching() {
|
func (s *AuthorizerSuite) TestShouldCheckUserMatching() {
|
||||||
|
@ -616,56 +773,56 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
|
||||||
|
|
||||||
results := tester.GetRuleMatchResults(John, "https://private.example.com", "GET")
|
results := tester.GetRuleMatchResults(John, "https://private.example.com", "GET")
|
||||||
|
|
||||||
require.Len(s.T(), results, 7)
|
s.Require().Len(results, 7)
|
||||||
|
|
||||||
assert.False(s.T(), results[0].IsMatch())
|
s.Assert().False(results[0].IsMatch())
|
||||||
assert.False(s.T(), results[0].MatchDomain)
|
s.Assert().False(results[0].MatchDomain)
|
||||||
assert.False(s.T(), results[0].MatchResources)
|
s.Assert().False(results[0].MatchResources)
|
||||||
assert.True(s.T(), results[0].MatchSubjects)
|
s.Assert().True(results[0].MatchSubjects)
|
||||||
assert.True(s.T(), results[0].MatchNetworks)
|
s.Assert().True(results[0].MatchNetworks)
|
||||||
assert.True(s.T(), results[0].MatchMethods)
|
s.Assert().True(results[0].MatchMethods)
|
||||||
|
|
||||||
assert.False(s.T(), results[1].IsMatch())
|
s.Assert().False(results[1].IsMatch())
|
||||||
assert.False(s.T(), results[1].MatchDomain)
|
s.Assert().False(results[1].MatchDomain)
|
||||||
assert.False(s.T(), results[1].MatchResources)
|
s.Assert().False(results[1].MatchResources)
|
||||||
assert.True(s.T(), results[1].MatchSubjects)
|
s.Assert().True(results[1].MatchSubjects)
|
||||||
assert.True(s.T(), results[1].MatchNetworks)
|
s.Assert().True(results[1].MatchNetworks)
|
||||||
assert.True(s.T(), results[1].MatchMethods)
|
s.Assert().True(results[1].MatchMethods)
|
||||||
|
|
||||||
assert.False(s.T(), results[2].IsMatch())
|
s.Assert().False(results[2].IsMatch())
|
||||||
assert.False(s.T(), results[2].MatchDomain)
|
s.Assert().False(results[2].MatchDomain)
|
||||||
assert.True(s.T(), results[2].MatchResources)
|
s.Assert().True(results[2].MatchResources)
|
||||||
assert.True(s.T(), results[2].MatchSubjects)
|
s.Assert().True(results[2].MatchSubjects)
|
||||||
assert.True(s.T(), results[2].MatchNetworks)
|
s.Assert().True(results[2].MatchNetworks)
|
||||||
assert.True(s.T(), results[2].MatchMethods)
|
s.Assert().True(results[2].MatchMethods)
|
||||||
|
|
||||||
assert.False(s.T(), results[3].IsMatch())
|
s.Assert().False(results[3].IsMatch())
|
||||||
assert.False(s.T(), results[3].MatchDomain)
|
s.Assert().False(results[3].MatchDomain)
|
||||||
assert.False(s.T(), results[3].MatchResources)
|
s.Assert().False(results[3].MatchResources)
|
||||||
assert.True(s.T(), results[3].MatchSubjects)
|
s.Assert().True(results[3].MatchSubjects)
|
||||||
assert.True(s.T(), results[3].MatchNetworks)
|
s.Assert().True(results[3].MatchNetworks)
|
||||||
assert.True(s.T(), results[3].MatchMethods)
|
s.Assert().True(results[3].MatchMethods)
|
||||||
|
|
||||||
assert.False(s.T(), results[4].IsMatch())
|
s.Assert().False(results[4].IsMatch())
|
||||||
assert.False(s.T(), results[4].MatchDomain)
|
s.Assert().False(results[4].MatchDomain)
|
||||||
assert.False(s.T(), results[4].MatchResources)
|
s.Assert().False(results[4].MatchResources)
|
||||||
assert.True(s.T(), results[4].MatchSubjects)
|
s.Assert().True(results[4].MatchSubjects)
|
||||||
assert.True(s.T(), results[4].MatchNetworks)
|
s.Assert().True(results[4].MatchNetworks)
|
||||||
assert.True(s.T(), results[4].MatchMethods)
|
s.Assert().True(results[4].MatchMethods)
|
||||||
|
|
||||||
assert.False(s.T(), results[5].IsMatch())
|
s.Assert().False(results[5].IsMatch())
|
||||||
assert.False(s.T(), results[5].MatchDomain)
|
s.Assert().False(results[5].MatchDomain)
|
||||||
assert.True(s.T(), results[5].MatchResources)
|
s.Assert().True(results[5].MatchResources)
|
||||||
assert.True(s.T(), results[5].MatchSubjects)
|
s.Assert().True(results[5].MatchSubjects)
|
||||||
assert.True(s.T(), results[5].MatchNetworks)
|
s.Assert().True(results[5].MatchNetworks)
|
||||||
assert.True(s.T(), results[5].MatchMethods)
|
s.Assert().True(results[5].MatchMethods)
|
||||||
|
|
||||||
assert.True(s.T(), results[6].IsMatch())
|
s.Assert().True(results[6].IsMatch())
|
||||||
assert.True(s.T(), results[6].MatchDomain)
|
s.Assert().True(results[6].MatchDomain)
|
||||||
assert.True(s.T(), results[6].MatchResources)
|
s.Assert().True(results[6].MatchResources)
|
||||||
assert.True(s.T(), results[6].MatchSubjects)
|
s.Assert().True(results[6].MatchSubjects)
|
||||||
assert.True(s.T(), results[6].MatchNetworks)
|
s.Assert().True(results[6].MatchNetworks)
|
||||||
assert.True(s.T(), results[6].MatchMethods)
|
s.Assert().True(results[6].MatchMethods)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestPolicyToLevel() {
|
func (s *AuthorizerSuite) TestPolicyToLevel() {
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegexpGroupStringSubjectMatcher matches the input string against the pattern taking into account Subexp groups.
|
||||||
|
type RegexpGroupStringSubjectMatcher struct {
|
||||||
|
Pattern regexp.Regexp
|
||||||
|
SubexpNameUser int
|
||||||
|
SubexpNameGroup int
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if the underlying pattern matches the input given the subject.
|
||||||
|
func (r RegexpGroupStringSubjectMatcher) IsMatch(input string, subject Subject) (match bool) {
|
||||||
|
matches := r.Pattern.FindAllStringSubmatch(input, -1)
|
||||||
|
if matches == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.SubexpNameUser != -1 && !strings.EqualFold(subject.Username, matches[0][r.SubexpNameUser]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.SubexpNameGroup != -1 && !utils.IsStringInSliceFold(matches[0][r.SubexpNameGroup], subject.Groups) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the pattern string.
|
||||||
|
func (r RegexpGroupStringSubjectMatcher) String() string {
|
||||||
|
return r.Pattern.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegexpStringSubjectMatcher just matches the input string against the pattern.
|
||||||
|
type RegexpStringSubjectMatcher struct {
|
||||||
|
Pattern regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if the underlying pattern matches the input.
|
||||||
|
func (r RegexpStringSubjectMatcher) IsMatch(input string, _ Subject) (match bool) {
|
||||||
|
return r.Pattern.MatchString(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the pattern string.
|
||||||
|
func (r RegexpStringSubjectMatcher) String() string {
|
||||||
|
return r.Pattern.String()
|
||||||
|
}
|
|
@ -12,10 +12,14 @@ type SubjectMatcher interface {
|
||||||
IsMatch(subject Subject) (match bool)
|
IsMatch(subject Subject) (match bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringSubjectMatcher is a matcher that takes an input string and subject.
|
||||||
|
type StringSubjectMatcher interface {
|
||||||
|
IsMatch(input string, subject Subject) (match bool)
|
||||||
|
}
|
||||||
|
|
||||||
// SubjectObjectMatcher is a matcher that takes both a subject and an object.
|
// SubjectObjectMatcher is a matcher that takes both a subject and an object.
|
||||||
type SubjectObjectMatcher interface {
|
type SubjectObjectMatcher interface {
|
||||||
IsMatch(subject Subject, object Object) (match bool)
|
IsMatch(subject Subject, object Object) (match bool)
|
||||||
String() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject represents the identity of a user for the purposes of ACL matching.
|
// Subject represents the identity of a user for the purposes of ACL matching.
|
||||||
|
|
|
@ -70,7 +70,7 @@ func schemaSubjectToACLSubject(subjectRule string) (subject SubjectMatcher) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp) (domains []SubjectObjectMatcher) {
|
func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp) (domains []AccessControlDomain) {
|
||||||
for _, domainRule := range domainRules {
|
for _, domainRule := range domainRules {
|
||||||
domains = append(domains, NewAccessControlDomain(domainRule))
|
domains = append(domains, NewAccessControlDomain(domainRule))
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp)
|
||||||
|
|
||||||
func schemaResourcesToACL(resourceRules []regexp.Regexp) (resources []AccessControlResource) {
|
func schemaResourcesToACL(resourceRules []regexp.Regexp) (resources []AccessControlResource) {
|
||||||
for _, resourceRule := range resourceRules {
|
for _, resourceRule := range resourceRules {
|
||||||
resources = append(resources, AccessControlResource{Pattern: resourceRule})
|
resources = append(resources, NewAccessControlResource(resourceRule))
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources
|
return resources
|
||||||
|
|
Loading…
Reference in New Issue