mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
feat(authorization): acl resource regex named groups (#3597)
This adds the named group functionality from domain_regex to the resource criteria.
This commit is contained in:
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
|
||||
[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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
return suffix == m.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
||||
default:
|
||||
return strings.EqualFold(domain, m.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a text representation of a AccessControlDomainRegexBasic.
|
||||
func (acl AccessControlDomainRegexBasic) String() string {
|
||||
return fmt.Sprintf("domain_regex:%s", acl.Pattern.String())
|
||||
// AccessControlDomain represents an ACL domain.
|
||||
type AccessControlDomain struct {
|
||||
Matcher StringSubjectMatcher
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (acl AccessControlDomainRegex) String() string {
|
||||
return fmt.Sprintf("domain_regex(subexp):%s", acl.Pattern.String())
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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() {
|
||||
|
|
53
internal/authorization/regexp.go
Normal file
53
internal/authorization/regexp.go
Normal 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()
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user