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
James Elliott 2022-06-28 12:51:05 +10:00 committed by GitHub
parent 19a543289b
commit ab1d0c51d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 366 additions and 168 deletions

View File

@ -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:

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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
} }
} }

View File

@ -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),

View File

@ -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() {

View File

@ -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()
}

View File

@ -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.

View File

@ -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