[FEATURE] Support for subject combinations in ACLs (#1142)
parent
b6a8b479fc
commit
5c4edf2f4d
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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").
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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{}
|
||||
|
|
Loading…
Reference in New Issue