From ab1d0c51d31e423f3caf4da1e02f3cc863c2cbd9 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 28 Jun 2022 12:51:05 +1000 Subject: [PATCH] feat(authorization): acl resource regex named groups (#3597) This adds the named group functionality from domain_regex to the resource criteria. --- .../configuration/security/access-control.md | 29 +- .../authorization/access_control_domain.go | 119 +++----- .../authorization/access_control_resource.go | 28 +- internal/authorization/access_control_rule.go | 8 +- internal/authorization/authorizer.go | 2 +- internal/authorization/authorizer_test.go | 285 ++++++++++++++---- internal/authorization/regexp.go | 53 ++++ internal/authorization/types.go | 6 +- internal/authorization/util.go | 4 +- 9 files changed, 366 insertions(+), 168 deletions(-) create mode 100644 internal/authorization/regexp.go diff --git a/docs/content/en/configuration/security/access-control.md b/docs/content/en/configuration/security/access-control.md index 32ba44151..bc3225854 100644 --- a/docs/content/en/configuration/security/access-control.md +++ b/docs/content/en/configuration/security/access-control.md @@ -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: diff --git a/internal/authorization/access_control_domain.go b/internal/authorization/access_control_domain.go index f028d1cd2..dfe72264b 100644 --- a/internal/authorization/access_control_domain.go +++ b/internal/authorization/access_control_domain.go @@ -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) } diff --git a/internal/authorization/access_control_resource.go b/internal/authorization/access_control_resource.go index 9bfca6d20..c9141f863 100644 --- a/internal/authorization/access_control_resource.go +++ b/internal/authorization/access_control_resource.go @@ -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) } diff --git a/internal/authorization/access_control_rule.go b/internal/authorization/access_control_rule.go index 29171e54c..3bcda3cc4 100644 --- a/internal/authorization/access_control_rule.go +++ b/internal/authorization/access_control_rule.go @@ -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 } } diff --git a/internal/authorization/authorizer.go b/internal/authorization/authorizer.go index 51bb49114..fc054d2ae 100644 --- a/internal/authorization/authorizer.go +++ b/internal/authorization/authorizer.go @@ -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), diff --git a/internal/authorization/authorizer_test.go b/internal/authorization/authorizer_test.go index adc3708b2..ea3bcdf33 100644 --- a/internal/authorization/authorizer_test.go +++ b/internal/authorization/authorizer_test.go @@ -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[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[2].DomainsRegex[0].String()) - assert.Equal(s.T(), "domain_regex(subexp):^(?P[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[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[3].DomainsRegex[0].String()) - assert.Equal(s.T(), "domain_regex(subexp):^group-(?P[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[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[a-zA-Z0-9]+)\\.regex.com$", ruleMatcher2.String()) + + s.Require().Len(tester.rules[3].Domains, 1) + + s.Assert().Equal("^group-(?P[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[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[a-zA-Z0-9]+)/personal(/|/.*)?$`, `^/(?P[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[a-zA-Z0-9]+)/personal(/|/.*)?$", ruleMatcher00.String()) + + ruleMatcher01, ok := tester.rules[0].Resources[1].Matcher.(RegexpGroupStringSubjectMatcher) + s.Require().True(ok) + s.Assert().Equal("^/(?P[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() { diff --git a/internal/authorization/regexp.go b/internal/authorization/regexp.go new file mode 100644 index 000000000..7983d842f --- /dev/null +++ b/internal/authorization/regexp.go @@ -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() +} diff --git a/internal/authorization/types.go b/internal/authorization/types.go index aa4579351..5644f39a6 100644 --- a/internal/authorization/types.go +++ b/internal/authorization/types.go @@ -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. diff --git a/internal/authorization/util.go b/internal/authorization/util.go index aedb6a9be..793c81d2e 100644 --- a/internal/authorization/util.go +++ b/internal/authorization/util.go @@ -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