[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 uniquely identified by `developers`, the subject should be `group:developers`. Similar to resources
and domains you can define multiple subjects in a single rule. 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 ## Networks
A list of network ranges can be specified in a rule in order to apply different policies when 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 - domain: dev.example.com
resources: resources:
- "^/users/john/.*$" - "^/users/john/.*$"
subject: "user:john" subject:
- ["group:dev", "user:john"]
policy: two_factor policy: two_factor
``` ```

View File

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

View File

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

View File

@ -6,25 +6,29 @@ import (
"github.com/authelia/authelia/internal/utils" "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 no subject is provided in the rule, we match any user.
if subjectRule == "" { if ruleSubject == "" {
return true continue
} }
if strings.HasPrefix(subjectRule, userPrefix) { if strings.HasPrefix(ruleSubject, userPrefix) {
user := strings.Trim(subjectRule[len(userPrefix):], " ") user := strings.Trim(ruleSubject[len(userPrefix):], " ")
if user == subject.Username { if user == subject.Username {
return true continue
} }
} }
if strings.HasPrefix(subjectRule, groupPrefix) { if strings.HasPrefix(ruleSubject, groupPrefix) {
group := strings.Trim(subjectRule[len(groupPrefix):], " ") group := strings.Trim(ruleSubject[len(groupPrefix):], " ")
if utils.IsStringInSlice(group, subject.Groups) { if utils.IsStringInSlice(group, subject.Groups) {
return true continue
} }
} }
return false return false
}
return true
} }

View File

@ -6,11 +6,11 @@ import (
"strings" "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 { type ACLRule struct {
Domains []string `mapstructure:"domain,weak"` Domains []string `mapstructure:"domain,weak"`
Policy string `mapstructure:"policy"` Policy string `mapstructure:"policy"`
Subjects []string `mapstructure:"subject,weak"` Subjects [][]string `mapstructure:"subject,weak"`
Networks []string `mapstructure:"networks"` Networks []string `mapstructure:"networks"`
Resources []string `mapstructure:"resources"` 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'")) 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) { 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"}, Domains: []string{"admin.example.com"},
Policy: "two_factor", Policy: "two_factor",
Subjects: []string{"group:admin"}, Subjects: [][]string{{"group:admin"}},
}, { }, {
Domains: []string{"grafana.example.com"}, Domains: []string{"grafana.example.com"},
Policy: "two_factor", Policy: "two_factor",
Subjects: []string{"group:grafana"}, Subjects: [][]string{{"group:grafana"}},
}} }}
providers := middlewares.Providers{} providers := middlewares.Providers{}