[FEATURE] Support for subject combinations in ACLs (#1142)

This commit is contained in:
Philipp Staiger 2020-06-25 10:22:42 +02:00 committed by GitHub
parent b6a8b479fc
commit 5c4edf2f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 34 deletions

View File

@ -70,6 +70,11 @@ For a user with unique identifier `john`, the subject should be `user:john` and
uniquely identified by `developers`, the subject should be `group:developers`. Similar to resources
and domains you can define multiple subjects in a single rule.
If you want a combination of subjects to be matched at once, you can specify a list of subjects like
`- ["group:developers", "group:admins"]`. Make sure to preceed it by a list key `-`.
In summary, the first level of subjects are evaluated using a logical `OR`, whereas the second level
by a logical `AND`.
## Networks
A list of network ranges can be specified in a rule in order to apply different policies when
@ -128,6 +133,7 @@ access_control:
- domain: dev.example.com
resources:
- "^/users/john/.*$"
subject: "user:john"
subject:
- ["group:dev", "user:john"]
policy: two_factor
```

View File

@ -138,12 +138,14 @@ func (p *Authorizer) IsURLMatchingRuleWithGroupSubjects(requestURL url.URL) (has
for _, rule := range p.configuration.Rules {
if isDomainMatching(requestURL.Hostname(), rule.Domains) && isPathMatching(requestURL.Path, rule.Resources) {
for _, subjectRule := range rule.Subjects {
if strings.HasPrefix(subjectRule, groupPrefix) {
for _, subject := range subjectRule {
if strings.HasPrefix(subject, groupPrefix) {
return true
}
}
}
}
}
return false
}

View File

@ -166,7 +166,7 @@ func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
WithRule(schema.ACLRule{
Domains: []string{"protected.example.com"},
Policy: "bypass",
Subjects: []string{"user:john"},
Subjects: [][]string{{"user:john"}},
}).
WithRule(schema.ACLRule{
Domains: []string{"protected.example.com"},
@ -189,7 +189,7 @@ func (s *AuthorizerSuite) TestShouldCheckUserMatching() {
WithRule(schema.ACLRule{
Domains: []string{"protected.example.com"},
Policy: "bypass",
Subjects: []string{"user:john"},
Subjects: [][]string{{"user:john"}},
}).
Build()
@ -203,7 +203,7 @@ func (s *AuthorizerSuite) TestShouldCheckGroupMatching() {
WithRule(schema.ACLRule{
Domains: []string{"protected.example.com"},
Policy: "bypass",
Subjects: []string{"group:admins"},
Subjects: [][]string{{"group:admins"}},
}).
Build()
@ -217,7 +217,7 @@ func (s *AuthorizerSuite) TestShouldCheckSubjectsMatching() {
WithRule(schema.ACLRule{
Domains: []string{"protected.example.com"},
Policy: "bypass",
Subjects: []string{"group:admins", "user:bob"},
Subjects: [][]string{{"group:admins"}, {"user:bob"}},
}).
Build()
@ -226,6 +226,21 @@ func (s *AuthorizerSuite) TestShouldCheckSubjectsMatching() {
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://protected.example.com/", Denied)
}
func (s *AuthorizerSuite) TestShouldCheckMultipleSubjectsMatching() {
tester := NewAuthorizerBuilder().
WithDefaultPolicy("deny").
WithRule(schema.ACLRule{
Domains: []string{"protected.example.com"},
Policy: "bypass",
Subjects: [][]string{{"group:admins", "user:bob"}, {"group:admins", "group:dev"}},
}).
Build()
tester.CheckAuthorizations(s.T(), John, "https://protected.example.com/", Bypass)
tester.CheckAuthorizations(s.T(), Bob, "https://protected.example.com/", Denied)
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://protected.example.com/", Denied)
}
func (s *AuthorizerSuite) TestShouldCheckIPMatching() {
tester := NewAuthorizerBuilder().
WithDefaultPolicy("deny").

View File

@ -6,25 +6,29 @@ import (
"github.com/authelia/authelia/internal/utils"
)
func isSubjectMatching(subject Subject, subjectRule string) bool {
func isSubjectMatching(subject Subject, subjectRule []string) bool {
for _, ruleSubject := range subjectRule {
// If no subject is provided in the rule, we match any user.
if subjectRule == "" {
return true
if ruleSubject == "" {
continue
}
if strings.HasPrefix(subjectRule, userPrefix) {
user := strings.Trim(subjectRule[len(userPrefix):], " ")
if strings.HasPrefix(ruleSubject, userPrefix) {
user := strings.Trim(ruleSubject[len(userPrefix):], " ")
if user == subject.Username {
return true
continue
}
}
if strings.HasPrefix(subjectRule, groupPrefix) {
group := strings.Trim(subjectRule[len(groupPrefix):], " ")
if strings.HasPrefix(ruleSubject, groupPrefix) {
group := strings.Trim(ruleSubject[len(groupPrefix):], " ")
if utils.IsStringInSlice(group, subject.Groups) {
return true
continue
}
}
return false
}
return true
}

View File

@ -6,11 +6,11 @@ import (
"strings"
)
// ACLRule represent one ACL rule "weak" coerces a single value into string slice.
// ACLRule represents one ACL rule entry; "weak" coerces a single value into slice.
type ACLRule struct {
Domains []string `mapstructure:"domain,weak"`
Policy string `mapstructure:"policy"`
Subjects []string `mapstructure:"subject,weak"`
Subjects [][]string `mapstructure:"subject,weak"`
Networks []string `mapstructure:"networks"`
Resources []string `mapstructure:"resources"`
}
@ -41,9 +41,11 @@ func (r *ACLRule) Validate(validator *StructValidator) {
validator.Push(fmt.Errorf("A policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
}
for i, subject := range r.Subjects {
for i, subjectRule := range r.Subjects {
for j, subject := range subjectRule {
if !IsSubjectValid(subject) {
validator.Push(fmt.Errorf("Subject %d must start with 'user:' or 'group:'", i))
validator.Push(fmt.Errorf("Subject %d-%d must start with 'user:' or 'group:'", i, j))
}
}
}

View File

@ -85,11 +85,11 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
}, {
Domains: []string{"admin.example.com"},
Policy: "two_factor",
Subjects: []string{"group:admin"},
Subjects: [][]string{{"group:admin"}},
}, {
Domains: []string{"grafana.example.com"},
Policy: "two_factor",
Subjects: []string{"group:grafana"},
Subjects: [][]string{{"group:grafana"}},
}}
providers := middlewares.Providers{}