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
[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
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).
In addition to standard regex patterns this criteria can match some [Named Regex Groups](#named-regex-groups).
#### 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`
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
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.*
@ -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
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
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.
func NewAccessControlDomain(domain string) SubjectObjectMatcher {
d := AccessControlDomain{}
func NewAccessControlDomain(domain string) AccessControlDomain {
m := &AccessControlDomainMatcher{}
domain = strings.ToLower(domain)
switch {
case strings.HasPrefix(domain, "*."):
d.Wildcard = true
d.Name = domain[1:]
m.Wildcard = true
m.Name = domain[1:]
case strings.HasPrefix(domain, "{user}"):
d.UserWildcard = true
d.Name = domain[7:]
m.UserWildcard = true
m.Name = domain[7:]
case strings.HasPrefix(domain, "{group}"):
d.GroupWildcard = true
d.Name = domain[8:]
m.GroupWildcard = true
m.Name = domain[8:]
default:
d.Name = domain
m.Name = domain
}
return d
}
// 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)
return AccessControlDomain{m}
}
// NewAccessControlDomainRegex creates a new SubjectObjectMatcher that matches the domain either in a basic way or
// dynamic User/Group subexpression group way.
func NewAccessControlDomainRegex(pattern regexp.Regexp) SubjectObjectMatcher {
func NewAccessControlDomainRegex(pattern regexp.Regexp) AccessControlDomain {
var iuser, igroup = -1, -1
for i, group := range pattern.SubexpNames() {
@ -75,53 +45,42 @@ func NewAccessControlDomainRegex(pattern regexp.Regexp) SubjectObjectMatcher {
}
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.
type AccessControlDomainRegexBasic struct {
Pattern regexp.Regexp
// AccessControlDomainMatcher is the basic domain matcher.
type AccessControlDomainMatcher struct {
Name string
Wildcard bool
UserWildcard bool
GroupWildcard bool
}
// IsMatch returns true if the ACL regex matches the object domain.
func (acl AccessControlDomainRegexBasic) IsMatch(_ Subject, object Object) (match bool) {
return acl.Pattern.MatchString(object.Domain)
}
// IsMatch returns true if this rule matches.
func (m AccessControlDomainMatcher) IsMatch(domain string, subject Subject) (match bool) {
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.
func (acl AccessControlDomainRegexBasic) String() string {
return fmt.Sprintf("domain_regex:%s", acl.Pattern.String())
}
// 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
return suffix == m.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
default:
return strings.EqualFold(domain, m.Name)
}
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.
func (acl AccessControlDomainRegex) String() string {
return fmt.Sprintf("domain_regex(subexp):%s", acl.Pattern.String())
// AccessControlDomain represents an ACL domain.
type AccessControlDomain struct {
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"
)
// 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 {
Pattern regexp.Regexp
Matcher StringSubjectMatcher
}
// IsMatch returns true if the ACL resource match the object path.
func (acr AccessControlResource) IsMatch(object Object) (match bool) {
return acr.Pattern.MatchString(object.Path)
func (acl AccessControlResource) IsMatch(subject Subject, object Object) (match bool) {
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.
type AccessControlRule struct {
Position int
Domains []SubjectObjectMatcher
Domains []AccessControlDomain
Resources []AccessControlResource
Methods []string
Networks []*net.IPNet
@ -48,7 +48,7 @@ func (acr *AccessControlRule) IsMatch(subject Subject, object Object) (match boo
return false
}
if !isMatchForResources(object, acr) {
if !isMatchForResources(subject, object, acr) {
return false
}
@ -83,7 +83,7 @@ func isMatchForDomains(subject Subject, object Object, acl *AccessControlRule) (
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 len(acl.Resources) == 0 {
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).
for _, resource := range acl.Resources {
if resource.IsMatch(object) {
if resource.IsMatch(subject, object) {
return true
}
}

View File

@ -79,7 +79,7 @@ func (p Authorizer) GetRuleMatchResults(subject Subject, object Object) (results
Skipped: skipped,
MatchDomain: isMatchForDomains(subject, object, rule),
MatchResources: isMatchForResources(object, rule),
MatchResources: isMatchForResources(subject, object, rule),
MatchMethods: isMatchForMethods(object, rule),
MatchNetworks: isMatchForNetworks(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)
}
func (s *AuthorizerSuite) TestShouldcheckDomainMatching() {
func (s *AuthorizerSuite) TestShouldCheckDomainMatching() {
tester := NewAuthorizerBuilder().
WithRule(schema.ACLRule{
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(), AnonymousUser, "https://x.example.com", "GET", OneFactor)
assert.Equal(s.T(), "public.example.com", tester.configuration.AccessControl.Rules[0].Domains[0])
assert.Equal(s.T(), "domain:public.example.com", tester.rules[0].Domains[0].String())
s.Require().Len(tester.rules, 5)
assert.Equal(s.T(), "one-factor.example.com", tester.configuration.AccessControl.Rules[1].Domains[0])
assert.Equal(s.T(), "domain:one-factor.example.com", tester.rules[1].Domains[0].String())
s.Require().Len(tester.rules[0].Domains, 1)
assert.Equal(s.T(), "two-factor.example.com", tester.configuration.AccessControl.Rules[2].Domains[0])
assert.Equal(s.T(), "domain:two-factor.example.com", tester.rules[2].Domains[0].String())
s.Assert().Equal("public.example.com", tester.configuration.AccessControl.Rules[0].Domains[0])
assert.Equal(s.T(), "*.example.com", tester.configuration.AccessControl.Rules[3].Domains[0])
assert.Equal(s.T(), "domain:.example.com", tester.rules[3].Domains[0].String())
ruleMatcher0, ok := tester.rules[0].Domains[0].Matcher.(*AccessControlDomainMatcher)
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])
assert.Equal(s.T(), "domain:.example.com", tester.rules[4].Domains[0].String())
s.Require().Len(tester.rules[1].Domains, 1)
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() {
@ -327,20 +369,135 @@ func (s *AuthorizerSuite) TestShouldCheckDomainRegexMatching() {
tester.CheckAuthorizations(s.T(), John, "https://group-dev.regex.com", "GET", TwoFactor)
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())
assert.Equal(s.T(), "domain_regex:^.*\\.example.com$", tester.rules[0].Domains[0].String())
s.Require().Len(tester.rules, 5)
assert.Equal(s.T(), "^.*\\.example2.com$", tester.configuration.AccessControl.Rules[1].DomainsRegex[0].String())
assert.Equal(s.T(), "domain_regex:^.*\\.example2.com$", tester.rules[1].Domains[0].String())
s.Require().Len(tester.rules[0].Domains, 1)
assert.Equal(s.T(), "^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[2].DomainsRegex[0].String())
assert.Equal(s.T(), "domain_regex(subexp):^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.rules[2].Domains[0].String())
s.Assert().Equal("^.*\\.example.com$", tester.configuration.AccessControl.Rules[0].DomainsRegex[0].String())
assert.Equal(s.T(), "^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[3].DomainsRegex[0].String())
assert.Equal(s.T(), "domain_regex(subexp):^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.rules[3].Domains[0].String())
ruleMatcher0, ok := tester.rules[0].Domains[0].Matcher.(RegexpStringSubjectMatcher)
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())
assert.Equal(s.T(), "domain_regex:^.*\\.(one|two).com$", tester.rules[4].Domains[0].String())
s.Require().Len(tester.rules[1].Domains, 1)
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() {
@ -616,56 +773,56 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
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())
assert.False(s.T(), results[0].MatchDomain)
assert.False(s.T(), results[0].MatchResources)
assert.True(s.T(), results[0].MatchSubjects)
assert.True(s.T(), results[0].MatchNetworks)
assert.True(s.T(), results[0].MatchMethods)
s.Assert().False(results[0].IsMatch())
s.Assert().False(results[0].MatchDomain)
s.Assert().False(results[0].MatchResources)
s.Assert().True(results[0].MatchSubjects)
s.Assert().True(results[0].MatchNetworks)
s.Assert().True(results[0].MatchMethods)
assert.False(s.T(), results[1].IsMatch())
assert.False(s.T(), results[1].MatchDomain)
assert.False(s.T(), results[1].MatchResources)
assert.True(s.T(), results[1].MatchSubjects)
assert.True(s.T(), results[1].MatchNetworks)
assert.True(s.T(), results[1].MatchMethods)
s.Assert().False(results[1].IsMatch())
s.Assert().False(results[1].MatchDomain)
s.Assert().False(results[1].MatchResources)
s.Assert().True(results[1].MatchSubjects)
s.Assert().True(results[1].MatchNetworks)
s.Assert().True(results[1].MatchMethods)
assert.False(s.T(), results[2].IsMatch())
assert.False(s.T(), results[2].MatchDomain)
assert.True(s.T(), results[2].MatchResources)
assert.True(s.T(), results[2].MatchSubjects)
assert.True(s.T(), results[2].MatchNetworks)
assert.True(s.T(), results[2].MatchMethods)
s.Assert().False(results[2].IsMatch())
s.Assert().False(results[2].MatchDomain)
s.Assert().True(results[2].MatchResources)
s.Assert().True(results[2].MatchSubjects)
s.Assert().True(results[2].MatchNetworks)
s.Assert().True(results[2].MatchMethods)
assert.False(s.T(), results[3].IsMatch())
assert.False(s.T(), results[3].MatchDomain)
assert.False(s.T(), results[3].MatchResources)
assert.True(s.T(), results[3].MatchSubjects)
assert.True(s.T(), results[3].MatchNetworks)
assert.True(s.T(), results[3].MatchMethods)
s.Assert().False(results[3].IsMatch())
s.Assert().False(results[3].MatchDomain)
s.Assert().False(results[3].MatchResources)
s.Assert().True(results[3].MatchSubjects)
s.Assert().True(results[3].MatchNetworks)
s.Assert().True(results[3].MatchMethods)
assert.False(s.T(), results[4].IsMatch())
assert.False(s.T(), results[4].MatchDomain)
assert.False(s.T(), results[4].MatchResources)
assert.True(s.T(), results[4].MatchSubjects)
assert.True(s.T(), results[4].MatchNetworks)
assert.True(s.T(), results[4].MatchMethods)
s.Assert().False(results[4].IsMatch())
s.Assert().False(results[4].MatchDomain)
s.Assert().False(results[4].MatchResources)
s.Assert().True(results[4].MatchSubjects)
s.Assert().True(results[4].MatchNetworks)
s.Assert().True(results[4].MatchMethods)
assert.False(s.T(), results[5].IsMatch())
assert.False(s.T(), results[5].MatchDomain)
assert.True(s.T(), results[5].MatchResources)
assert.True(s.T(), results[5].MatchSubjects)
assert.True(s.T(), results[5].MatchNetworks)
assert.True(s.T(), results[5].MatchMethods)
s.Assert().False(results[5].IsMatch())
s.Assert().False(results[5].MatchDomain)
s.Assert().True(results[5].MatchResources)
s.Assert().True(results[5].MatchSubjects)
s.Assert().True(results[5].MatchNetworks)
s.Assert().True(results[5].MatchMethods)
assert.True(s.T(), results[6].IsMatch())
assert.True(s.T(), results[6].MatchDomain)
assert.True(s.T(), results[6].MatchResources)
assert.True(s.T(), results[6].MatchSubjects)
assert.True(s.T(), results[6].MatchNetworks)
assert.True(s.T(), results[6].MatchMethods)
s.Assert().True(results[6].IsMatch())
s.Assert().True(results[6].MatchDomain)
s.Assert().True(results[6].MatchResources)
s.Assert().True(results[6].MatchSubjects)
s.Assert().True(results[6].MatchNetworks)
s.Assert().True(results[6].MatchMethods)
}
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)
}
// 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.
type SubjectObjectMatcher interface {
IsMatch(subject Subject, object Object) (match bool)
String() string
}
// 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
}
func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp) (domains []SubjectObjectMatcher) {
func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp) (domains []AccessControlDomain) {
for _, domainRule := range domainRules {
domains = append(domains, NewAccessControlDomain(domainRule))
}
@ -84,7 +84,7 @@ func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp)
func schemaResourcesToACL(resourceRules []regexp.Regexp) (resources []AccessControlResource) {
for _, resourceRule := range resourceRules {
resources = append(resources, AccessControlResource{Pattern: resourceRule})
resources = append(resources, NewAccessControlResource(resourceRule))
}
return resources