[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
|
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
|
||||||
```
|
```
|
||||||
|
|
|
@ -138,8 +138,10 @@ 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 {
|
||||||
return true
|
if strings.HasPrefix(subject, groupPrefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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").
|
||||||
|
|
|
@ -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 {
|
||||||
// If no subject is provided in the rule, we match any user.
|
for _, ruleSubject := range subjectRule {
|
||||||
if subjectRule == "" {
|
// If no subject is provided in the rule, we match any user.
|
||||||
return true
|
if ruleSubject == "" {
|
||||||
}
|
continue
|
||||||
|
|
||||||
if strings.HasPrefix(subjectRule, userPrefix) {
|
|
||||||
user := strings.Trim(subjectRule[len(userPrefix):], " ")
|
|
||||||
if user == subject.Username {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(subjectRule, groupPrefix) {
|
if strings.HasPrefix(ruleSubject, userPrefix) {
|
||||||
group := strings.Trim(subjectRule[len(groupPrefix):], " ")
|
user := strings.Trim(ruleSubject[len(userPrefix):], " ")
|
||||||
if utils.IsStringInSlice(group, subject.Groups) {
|
if user == subject.Username {
|
||||||
return true
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(ruleSubject, groupPrefix) {
|
||||||
|
group := strings.Trim(ruleSubject[len(groupPrefix):], " ")
|
||||||
|
if utils.IsStringInSlice(group, subject.Groups) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,13 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPolicyValid check if policy is valid.
|
// IsPolicyValid check if policy is valid.
|
||||||
|
@ -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 {
|
||||||
if !IsSubjectValid(subject) {
|
for j, subject := range subjectRule {
|
||||||
validator.Push(fmt.Errorf("Subject %d must start with 'user:' or 'group:'", i))
|
if !IsSubjectValid(subject) {
|
||||||
|
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"},
|
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{}
|
||||||
|
|
Loading…
Reference in New Issue