feat(authorization): domain regex match with named groups (#2789)
This adds an option to match domains by regex including two special named matching groups. User matches the username of the user, and Group matches the groups a user is a member of. These are both case-insensitive and you can see examples in the docs.pull/2785/head^2
parent
0116506330
commit
3c1bb3ec19
|
@ -366,6 +366,18 @@ access_control:
|
|||
- domain: public.example.com
|
||||
policy: bypass
|
||||
|
||||
## Domain Regex examples. Generally we recommend just using a standard domain.
|
||||
# - domain_regex: "^(?P<User>\w+)\\.example\\.com$"
|
||||
# policy: one_factor
|
||||
# - domain_regex: "^(?P<Group>\w+)\\.example\\.com$"
|
||||
# policy: one_factor
|
||||
# - domain_regex:
|
||||
# - "^appgroup-.*\\.example\\.com$"
|
||||
# - "^appgroup2-.*\\.example\\.com$"
|
||||
# policy: one_factor
|
||||
# - domain_regex: "^.*\\.example.com$"
|
||||
# policy: two_factor
|
||||
|
||||
- domain: secure.example.com
|
||||
policy: one_factor
|
||||
## Network based rule, if not provided any network matches.
|
||||
|
@ -397,21 +409,21 @@ access_control:
|
|||
## Rules applied to 'dev' group
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/groups/dev/.*$"
|
||||
- '^/groups/dev/.*$'
|
||||
subject: "group:dev"
|
||||
policy: two_factor
|
||||
|
||||
## Rules applied to user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/john/.*$"
|
||||
- '^/users/john/.*$'
|
||||
subject: "user:john"
|
||||
policy: two_factor
|
||||
|
||||
## Rules applied to user 'harry'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/harry/.*$"
|
||||
- '^/users/harry/.*$'
|
||||
subject: "user:harry"
|
||||
policy: two_factor
|
||||
|
||||
|
@ -421,7 +433,7 @@ access_control:
|
|||
policy: two_factor
|
||||
- domain: "dev.example.com"
|
||||
resources:
|
||||
- "^/users/bob/.*$"
|
||||
- '^/users/bob/.*$'
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ access_control:
|
|||
|
||||
rules:
|
||||
- domain: public.example.com
|
||||
domain_regex: "^\d+\\.public.example.com$"
|
||||
policy: one_factor
|
||||
networks:
|
||||
- internal
|
||||
|
@ -35,7 +36,7 @@ access_control:
|
|||
- GET
|
||||
- HEAD
|
||||
resources:
|
||||
- "^/api.*"
|
||||
- '^/api.*'
|
||||
```
|
||||
|
||||
## Options
|
||||
|
@ -96,6 +97,7 @@ A rule defines two primary things:
|
|||
The criteria is broken into several parts:
|
||||
|
||||
* [domain](#domain): domain or list of domains targeted by the request.
|
||||
* [domain_regex](#domain_regex): regex form of [domain](#domain).
|
||||
* [resources](#resources): pattern or list of patterns that the path should match.
|
||||
* [subject](#subject): the user or group of users to define the policy for.
|
||||
* [networks](#networks): the network addresses, ranges (CIDR notation) or groups from where the request originates.
|
||||
|
@ -107,17 +109,6 @@ is a match for a given request is the rule applied; subsequent rules have *no ef
|
|||
carefully evaluate your rule list **in order** to see which rule matches a particular scenario. A comprehensive
|
||||
understanding of how rules apply is also recommended.
|
||||
|
||||
#### policy
|
||||
<div markdown="1">
|
||||
type: string
|
||||
{: .label .label-config .label-purple }
|
||||
required: yes
|
||||
{: .label .label-config .label-red }
|
||||
</div>
|
||||
|
||||
The specific [policy](#policies) to apply to the selected rule. This is not criteria for a match, this is the action to
|
||||
take when a match is made.
|
||||
|
||||
#### domain
|
||||
<div markdown="1">
|
||||
type: list(string)
|
||||
|
@ -126,8 +117,12 @@ required: yes
|
|||
{: .label .label-config .label-red }
|
||||
</div>
|
||||
|
||||
_**Required:** This criteria OR the [domain_regex](#domain_regex) criteria are required._
|
||||
|
||||
This criteria matches the domain name and has two methods of configuration, either as a single string or as a list of
|
||||
strings. When it's a list of strings the rule matches when **any** of the domains in the list match the request domain.
|
||||
When used in conjunction with [domain_regex](#domain_regex) the rule will match when either the [domain](#domain) or the
|
||||
[domain_regex](#domain_regex) criteria matches.
|
||||
|
||||
Rules may start with a few different wildcards:
|
||||
|
||||
|
@ -136,12 +131,17 @@ Rules may start with a few different wildcards:
|
|||
string **must** be quoted like `"*.example.com"`.
|
||||
|
||||
* The user wildcard is `{user}.`, which when in front of a domain dynamically matches the username of the user. For
|
||||
example `{user}.example.com` would match `fred.example.com` if the user logged in was named `fred`. ***Note:** we're
|
||||
considering refactoring this to just be regex which would likely allow many additional possibilities.*
|
||||
example `{user}.example.com` would match `fred.example.com` if the user logged in was named `fred`. _**Warning:** this is
|
||||
officially deprecated as the [domain_regex](#domain_regex) criteria completely replaces the functionality in a much
|
||||
more useful way. It is strongly recommended you do not use this as it will be removed in a future version, most likely
|
||||
v5.0.0._
|
||||
|
||||
* The group wildcard is `{group}.`, which when in front of a domain dynamically matches if the logged in user has the
|
||||
group in that location. For example `{group}.example.com` would match `admins.example.com` if the user logged in was
|
||||
in the following groups `admins,users,people` because `admins` is in the list.
|
||||
in the following groups `admins,users,people` because `admins` is in the list. _**Warning:** this is
|
||||
officially deprecated as the [domain_regex](#domain_regex) criteria completely replaces the functionality in a much
|
||||
more useful way. It is strongly recommended you do not use this as it will be removed in a future version, most likely
|
||||
v5.0.0._
|
||||
|
||||
Domains in this section must be the domain configured in the [session](./session/index.md#domain) configuration or
|
||||
subdomains of that domain. This is because a website can only write cookies for a domain it is part of. It is
|
||||
|
@ -177,6 +177,66 @@ access_control:
|
|||
policy: bypass
|
||||
```
|
||||
|
||||
### domain_regex
|
||||
<div markdown="1">
|
||||
type: list(string)
|
||||
{: .label .label-config .label-purple }
|
||||
required: yes
|
||||
{: .label .label-config .label-red }
|
||||
</div>
|
||||
|
||||
_**Required:** This criteria OR the [domain](#domain) criteria are required._
|
||||
|
||||
_**Important Note:** If you intend to use this criteria with a bypass rule please read
|
||||
[bypass and subjects](#bypass-and-user-identity) before doing so._
|
||||
|
||||
This criteria matches the domain name and has two methods of configuration, either as a single string or as a list of
|
||||
strings. When it's a list of strings the rule matches when **any** of the domains in the list match the request 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.
|
||||
|
||||
As this is a regex string you will either need to use single quotes or need to double-escape certain portions of the
|
||||
regex in order make this work.
|
||||
|
||||
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://datatracker.ietf.org/doc/html/rfc4343) abstract and
|
||||
[RFC3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2) section 3.2.2.
|
||||
|
||||
Examples:
|
||||
|
||||
```yaml
|
||||
access_control:
|
||||
rules:
|
||||
- domain:
|
||||
- apple.example.com
|
||||
- banana.example.com
|
||||
policy: bypass
|
||||
- domain_regex:
|
||||
- "^user-(?P<User>\w+)\\.example\\.com$"
|
||||
- "^group-(?P<Group>\w+)\\.example\\.com$"
|
||||
policy: one_factor
|
||||
```
|
||||
|
||||
#### policy
|
||||
<div markdown="1">
|
||||
type: string
|
||||
{: .label .label-config .label-purple }
|
||||
required: yes
|
||||
{: .label .label-config .label-red }
|
||||
</div>
|
||||
|
||||
The specific [policy](#policies) to apply to the selected rule. This is not criteria for a match, this is the action to
|
||||
take when a match is made.
|
||||
|
||||
### subject
|
||||
<div markdown="1">
|
||||
type: list(list(string))
|
||||
|
@ -191,10 +251,10 @@ result in problematic security scenarios with badly thought out configurations a
|
|||
scenario that would require users to do this. If you have a scenario in mind please open an
|
||||
[issue](https://github.com/authelia/authelia/issues/new) on GitHub.*
|
||||
|
||||
This criteria matches identifying characteristics about the subject. Currently this is either user or groups the user
|
||||
belongs to. This allows you to effectively control exactly what each user is authorized to access or to specifically
|
||||
require two-factor authentication to specific users. Subjects are prefixed with either `user:` or `group:` to identify
|
||||
which part of the identity to check.
|
||||
This criteria matches identifying characteristics about the subject. This is either user's name or the name of the
|
||||
group a user belongs to. This allows you to effectively control exactly what each user is authorized to access or to
|
||||
specifically require two-factor authentication to specific users. Subjects are prefixed with either `user:` or `group:`
|
||||
to identify which part of the identity to check.
|
||||
|
||||
The format of this rule is unique in as much as it is a list of lists. The logic behind this format is to allow for both
|
||||
`OR` and `AND` logic. The first level of the list defines the `OR` logic, and the second level defines the `AND` logic.
|
||||
|
@ -355,9 +415,9 @@ for debugging these regular expressions is called [Rego](https://regoio.herokuap
|
|||
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.*
|
||||
|
||||
It's important when configuring resource rules that you enclose them in quotes otherwise you may run into some issues
|
||||
with escaping the expressions. Failure to do so may prevent Authelia from starting. It's technically optional but will
|
||||
likely save you a lot of time if you do it for all resource rules.
|
||||
As this is a regex string you will either need to use single quotes or need to double-escape certain portions of the
|
||||
regex in order make this work. If you don't do either of these things either the regex may not be parsed, or it may not
|
||||
be parsed correctly. It's technically optional but will likely save you a lot of time if you do it for all resource rules.
|
||||
|
||||
Examples:
|
||||
|
||||
|
@ -370,16 +430,13 @@ access_control:
|
|||
- domain: app.example.com
|
||||
policy: bypass
|
||||
resources:
|
||||
- "^/api([/?].*)?$"
|
||||
- '^/api([/?].*)?$'
|
||||
```
|
||||
|
||||
## Policies
|
||||
|
||||
With **Authelia** you can define a list of rules that are going to be evaluated in
|
||||
sequential order when authorization is delegated to Authelia.
|
||||
|
||||
The first matching rule of the list defines the policy applied to the resource, if
|
||||
no rule matches the resource a customizable default policy is applied.
|
||||
The policy of the first matching rule in the configured list decides the policy applied to the request, if no rule
|
||||
matches the request the [default_policy](#default_policy) is applied.
|
||||
|
||||
### deny
|
||||
|
||||
|
@ -393,6 +450,18 @@ This policy skips all authentication and allows anyone to use the resource. This
|
|||
that includes a [subject](#subject) restriction because the minimum authentication level required to obtain information
|
||||
about the subject is [one_factor](#one_factor).
|
||||
|
||||
#### bypass and user identity
|
||||
|
||||
The [bypass](#bypass) policy cannot be used when the rule uses a criteria that requires we know the users identity. This
|
||||
means:
|
||||
|
||||
- If the rule defines [subjects](#subject) criteria
|
||||
- If the rule defines [domain regex](#domain_regex) criteria which contains either the user or group named match groups
|
||||
|
||||
This is because these criteria types require knowing who the user is in order to determine if their identity matches the
|
||||
request. This information can only be known after 1FA, which means the minimum policy that can be used logically is
|
||||
[one_factor](#one_factor).
|
||||
|
||||
### one_factor
|
||||
|
||||
This policy requires the user at minimum complete 1FA successfully (username and password). This means if they have
|
||||
|
@ -454,13 +523,13 @@ access_control:
|
|||
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/groups/dev/.*$"
|
||||
- '^/groups/dev/.*$'
|
||||
subject: "group:dev"
|
||||
policy: two_factor
|
||||
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/john/.*$"
|
||||
- '^/users/john/.*$'
|
||||
subject:
|
||||
- ["group:dev", "user:john"]
|
||||
- "group:admins"
|
||||
|
|
|
@ -2,11 +2,35 @@ package authorization
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// NewAccessControlDomain creates a new SubjectObjectMatcher that matches the domain as a basic string.
|
||||
func NewAccessControlDomain(domain string) SubjectObjectMatcher {
|
||||
d := AccessControlDomain{}
|
||||
|
||||
domain = strings.ToLower(domain)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(domain, "*."):
|
||||
d.Wildcard = true
|
||||
d.Name = domain[1:]
|
||||
case strings.HasPrefix(domain, "{user}"):
|
||||
d.UserWildcard = true
|
||||
d.Name = domain[7:]
|
||||
case strings.HasPrefix(domain, "{group}"):
|
||||
d.GroupWildcard = true
|
||||
d.Name = domain[8:]
|
||||
default:
|
||||
d.Name = domain
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// AccessControlDomain represents an ACL domain.
|
||||
type AccessControlDomain struct {
|
||||
Name string
|
||||
|
@ -16,17 +40,88 @@ type AccessControlDomain struct {
|
|||
}
|
||||
|
||||
// IsMatch returns true if the ACL domain matches the object domain.
|
||||
func (acd AccessControlDomain) IsMatch(subject Subject, object Object) (match bool) {
|
||||
func (acl AccessControlDomain) IsMatch(subject Subject, object Object) (match bool) {
|
||||
switch {
|
||||
case acd.Wildcard:
|
||||
return strings.HasSuffix(object.Domain, acd.Name)
|
||||
case acd.UserWildcard:
|
||||
return object.Domain == fmt.Sprintf("%s.%s", subject.Username, acd.Name)
|
||||
case acd.GroupWildcard:
|
||||
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 == acd.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
||||
return suffix == acl.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
||||
default:
|
||||
return object.Domain == acd.Name
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 AccessControlDomainRegex{Pattern: pattern, SubexpNameUser: iuser, SubexpNameGroup: igroup}
|
||||
}
|
||||
|
||||
return AccessControlDomainRegexBasic{Pattern: pattern}
|
||||
}
|
||||
|
||||
// AccessControlDomainRegexBasic represents a basic domain regex SubjectObjectMatcher.
|
||||
type AccessControlDomainRegexBasic struct {
|
||||
Pattern regexp.Regexp
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
// AccessControlResource represents an ACL resource.
|
||||
type AccessControlResource struct {
|
||||
Pattern *regexp.Regexp
|
||||
Pattern regexp.Regexp
|
||||
}
|
||||
|
||||
// IsMatch returns true if the ACL resource match the object path.
|
||||
|
|
|
@ -22,7 +22,7 @@ func NewAccessControlRules(config schema.AccessControlConfiguration) (rules []*A
|
|||
func NewAccessControlRule(pos int, rule schema.ACLRule, networksMap map[string][]*net.IPNet, networksCacheMap map[string]*net.IPNet) *AccessControlRule {
|
||||
return &AccessControlRule{
|
||||
Position: pos,
|
||||
Domains: schemaDomainsToACL(rule.Domains),
|
||||
Domains: schemaDomainsToACL(rule.Domains, rule.DomainsRegex),
|
||||
Resources: schemaResourcesToACL(rule.Resources),
|
||||
Methods: schemaMethodsToACL(rule.Methods),
|
||||
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
|
||||
|
@ -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 []AccessControlDomain
|
||||
Domains []SubjectObjectMatcher
|
||||
Resources []AccessControlResource
|
||||
Methods []string
|
||||
Networks []*net.IPNet
|
||||
|
|
|
@ -4,14 +4,9 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// AccessControlSubject abstracts an ACL subject of type `group:` or `user:`.
|
||||
type AccessControlSubject interface {
|
||||
IsMatch(subject Subject) (match bool)
|
||||
}
|
||||
|
||||
// AccessControlSubjects represents an ACL subject.
|
||||
type AccessControlSubjects struct {
|
||||
Subjects []AccessControlSubject
|
||||
Subjects []SubjectMatcher
|
||||
}
|
||||
|
||||
// AddSubject appends to the AccessControlSubjects based on a subject rule string.
|
||||
|
|
|
@ -3,6 +3,7 @@ package authorization
|
|||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -230,6 +231,118 @@ func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
|
|||
tester.CheckAuthorizations(s.T(), John, "https://public.example.com/", "GET", TwoFactor)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldcheckDomainMatching() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"public.example.com"},
|
||||
Policy: bypass,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"one-factor.example.com"},
|
||||
Policy: oneFactor,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"two-factor.example.com"},
|
||||
Policy: twoFactor,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"*.example.com"},
|
||||
Policy: oneFactor,
|
||||
Subjects: [][]string{{"group:admins"}},
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"*.example.com"},
|
||||
Policy: twoFactor,
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://public.example.com", "GET", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://public.example.com", "GET", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://public.example.com", "GET", Bypass)
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://one-factor.example.com", "GET", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://one-factor.example.com", "GET", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://one-factor.example.com", "GET", OneFactor)
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://two-factor.example.com", "GET", TwoFactor)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://two-factor.example.com", "GET", TwoFactor)
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://two-factor.example.com", "GET", TwoFactor)
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://x.example.com", "GET", OneFactor)
|
||||
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())
|
||||
|
||||
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())
|
||||
|
||||
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())
|
||||
|
||||
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())
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckDomainRegexMatching() {
|
||||
createSliceRegexRule := func(t *testing.T, rules []string) []regexp.Regexp {
|
||||
result, err := stringSliceToRegexpSlice(rules)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithRule(schema.ACLRule{
|
||||
DomainsRegex: createSliceRegexRule(s.T(), []string{`^.*\.example.com$`}),
|
||||
Policy: bypass,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
DomainsRegex: createSliceRegexRule(s.T(), []string{`^.*\.example2.com$`}),
|
||||
Policy: oneFactor,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
DomainsRegex: createSliceRegexRule(s.T(), []string{`^(?P<User>[a-zA-Z0-9]+)\.regex.com$`}),
|
||||
Policy: oneFactor,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
DomainsRegex: createSliceRegexRule(s.T(), []string{`^group-(?P<Group>[a-zA-Z0-9]+)\.regex.com$`}),
|
||||
Policy: twoFactor,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
DomainsRegex: createSliceRegexRule(s.T(), []string{`^.*\.(one|two).com$`}),
|
||||
Policy: twoFactor,
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://john.regex.com", "GET", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://john.regex.com", "GET", Denied)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://public.example.com", "GET", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://public.example2.com", "GET", OneFactor)
|
||||
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())
|
||||
|
||||
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())
|
||||
|
||||
assert.Equal(s.T(), "^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[2].DomainsRegex[0].String())
|
||||
assert.Equal(s.T(), "domain_regex(subexp):^(?P<User>[a-zA-Z0-9]+)\\.regex.com$", tester.rules[2].Domains[0].String())
|
||||
|
||||
assert.Equal(s.T(), "^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.configuration.AccessControl.Rules[3].DomainsRegex[0].String())
|
||||
assert.Equal(s.T(), "domain_regex(subexp):^group-(?P<Group>[a-zA-Z0-9]+)\\.regex.com$", tester.rules[3].Domains[0].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())
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckUserMatching() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy(deny).
|
||||
|
@ -367,17 +480,25 @@ func (s *AuthorizerSuite) TestShouldCheckMethodMatching() {
|
|||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckResourceMatching() {
|
||||
createSliceRegexRule := func(t *testing.T, rules []string) []regexp.Regexp {
|
||||
result, err := stringSliceToRegexpSlice(rules)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy(deny).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"resource.example.com"},
|
||||
Policy: bypass,
|
||||
Resources: []string{"^/bypass/[a-z]+$", "^/$", "embedded"},
|
||||
Resources: createSliceRegexRule(s.T(), []string{"^/bypass/[a-z]+$", "^/$", "embedded"}),
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"resource.example.com"},
|
||||
Policy: oneFactor,
|
||||
Resources: []string{"^/one_factor/[a-z]+$"},
|
||||
Resources: createSliceRegexRule(s.T(), []string{"^/one_factor/[a-z]+$"}),
|
||||
}).
|
||||
Build()
|
||||
|
||||
|
@ -424,17 +545,25 @@ func (s *AuthorizerSuite) TestShouldMatchAnyDomainIfBlank() {
|
|||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
|
||||
createSliceRegexRule := func(t *testing.T, rules []string) []regexp.Regexp {
|
||||
result, err := stringSliceToRegexpSlice(rules)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy(deny).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"public.example.com"},
|
||||
Resources: []string{"^/admin/.*$"},
|
||||
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||
Subjects: [][]string{{"group:admins"}},
|
||||
Policy: oneFactor,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"public.example.com"},
|
||||
Resources: []string{"^/admin/.*$"},
|
||||
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||
Policy: deny,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
|
@ -443,13 +572,13 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
|
|||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"public2.example.com"},
|
||||
Resources: []string{"^/admin/.*$"},
|
||||
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||
Subjects: [][]string{{"group:admins"}},
|
||||
Policy: bypass,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domains: []string{"public2.example.com"},
|
||||
Resources: []string{"^/admin/.*$"},
|
||||
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||
Policy: deny,
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
|
|
|
@ -14,12 +14,26 @@ const (
|
|||
Denied Level = iota
|
||||
)
|
||||
|
||||
const userPrefix = "user:"
|
||||
const groupPrefix = "group:"
|
||||
const (
|
||||
prefixUser = "user:"
|
||||
prefixGroup = "group:"
|
||||
)
|
||||
|
||||
const bypass = "bypass"
|
||||
const oneFactor = "one_factor"
|
||||
const twoFactor = "two_factor"
|
||||
const deny = "deny"
|
||||
const (
|
||||
bypass = "bypass"
|
||||
oneFactor = "one_factor"
|
||||
twoFactor = "two_factor"
|
||||
deny = "deny"
|
||||
)
|
||||
|
||||
const (
|
||||
subexpNameUser = "User"
|
||||
subexpNameGroup = "Group"
|
||||
)
|
||||
|
||||
var (
|
||||
// IdentitySubexpNames is a list of valid regex subexp names.
|
||||
IdentitySubexpNames = []string{subexpNameUser, subexpNameGroup}
|
||||
)
|
||||
|
||||
const traceFmtACLHitMiss = "ACL %s Position %d for subject %s and object %s (Method %s)"
|
||||
|
|
|
@ -7,6 +7,17 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// SubjectMatcher is a matcher that takes a subject.
|
||||
type SubjectMatcher interface {
|
||||
IsMatch(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.
|
||||
type Subject struct {
|
||||
Username string
|
||||
|
|
|
@ -20,3 +20,16 @@ func TestShouldAppendQueryParamToURL(t *testing.T) {
|
|||
assert.Equal(t, "/api?type=none", object.Path)
|
||||
assert.Equal(t, "https", object.Scheme)
|
||||
}
|
||||
|
||||
func TestShouldCreateNewObjectFromRaw(t *testing.T) {
|
||||
targetURL, err := url.Parse("https://domain.example.com/api")
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
object := NewObjectRaw(targetURL, []byte("GET"))
|
||||
|
||||
assert.Equal(t, "domain.example.com", object.Domain)
|
||||
assert.Equal(t, "GET", object.Method)
|
||||
assert.Equal(t, "/api", object.Path)
|
||||
assert.Equal(t, "https", object.Scheme)
|
||||
}
|
||||
|
|
|
@ -41,15 +41,28 @@ func LevelToPolicy(level Level) (policy string) {
|
|||
return deny
|
||||
}
|
||||
|
||||
func schemaSubjectToACLSubject(subjectRule string) (subject AccessControlSubject) {
|
||||
if strings.HasPrefix(subjectRule, userPrefix) {
|
||||
user := strings.Trim(subjectRule[len(userPrefix):], " ")
|
||||
func stringSliceToRegexpSlice(strings []string) (regexps []regexp.Regexp, err error) {
|
||||
for _, str := range strings {
|
||||
pattern, err := regexp.Compile(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
regexps = append(regexps, *pattern)
|
||||
}
|
||||
|
||||
return regexps, nil
|
||||
}
|
||||
|
||||
func schemaSubjectToACLSubject(subjectRule string) (subject SubjectMatcher) {
|
||||
if strings.HasPrefix(subjectRule, prefixUser) {
|
||||
user := strings.Trim(subjectRule[len(prefixUser):], " ")
|
||||
|
||||
return AccessControlUser{Name: user}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(subjectRule, groupPrefix) {
|
||||
group := strings.Trim(subjectRule[len(groupPrefix):], " ")
|
||||
if strings.HasPrefix(subjectRule, prefixGroup) {
|
||||
group := strings.Trim(subjectRule[len(prefixGroup):], " ")
|
||||
|
||||
return AccessControlGroup{Name: group}
|
||||
}
|
||||
|
@ -57,35 +70,21 @@ func schemaSubjectToACLSubject(subjectRule string) (subject AccessControlSubject
|
|||
return nil
|
||||
}
|
||||
|
||||
func schemaDomainsToACL(domainRules []string) (domains []AccessControlDomain) {
|
||||
func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp) (domains []SubjectObjectMatcher) {
|
||||
for _, domainRule := range domainRules {
|
||||
domain := AccessControlDomain{}
|
||||
|
||||
domainRule = strings.ToLower(domainRule)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(domainRule, "*."):
|
||||
domain.Wildcard = true
|
||||
domain.Name = domainRule[1:]
|
||||
case strings.HasPrefix(domainRule, "{user}"):
|
||||
domain.UserWildcard = true
|
||||
domain.Name = domainRule[7:]
|
||||
case strings.HasPrefix(domainRule, "{group}"):
|
||||
domain.GroupWildcard = true
|
||||
domain.Name = domainRule[8:]
|
||||
default:
|
||||
domain.Name = domainRule
|
||||
domains = append(domains, NewAccessControlDomain(domainRule))
|
||||
}
|
||||
|
||||
domains = append(domains, domain)
|
||||
for _, domainRegexRule := range domainRegexRules {
|
||||
domains = append(domains, NewAccessControlDomainRegex(domainRegexRule))
|
||||
}
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
func schemaResourcesToACL(resourceRules []string) (resources []AccessControlResource) {
|
||||
func schemaResourcesToACL(resourceRules []regexp.Regexp) (resources []AccessControlResource) {
|
||||
for _, resourceRule := range resourceRules {
|
||||
resources = append(resources, AccessControlResource{regexp.MustCompile(resourceRule)})
|
||||
resources = append(resources, AccessControlResource{Pattern: resourceRule})
|
||||
}
|
||||
|
||||
return resources
|
||||
|
|
|
@ -185,6 +185,9 @@ func TestShouldParseACLNetworks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldReturnCorrectValidationLevel(t *testing.T) {
|
||||
assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Denied))
|
||||
assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, Denied))
|
||||
assert.False(t, IsAuthLevelSufficient(authentication.TwoFactor, Denied))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Bypass))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, Bypass))
|
||||
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, Bypass))
|
||||
|
|
|
@ -366,6 +366,18 @@ access_control:
|
|||
- domain: public.example.com
|
||||
policy: bypass
|
||||
|
||||
## Domain Regex examples. Generally we recommend just using a standard domain.
|
||||
# - domain_regex: "^(?P<User>\w+)\\.example\\.com$"
|
||||
# policy: one_factor
|
||||
# - domain_regex: "^(?P<Group>\w+)\\.example\\.com$"
|
||||
# policy: one_factor
|
||||
# - domain_regex:
|
||||
# - "^appgroup-.*\\.example\\.com$"
|
||||
# - "^appgroup2-.*\\.example\\.com$"
|
||||
# policy: one_factor
|
||||
# - domain_regex: "^.*\\.example.com$"
|
||||
# policy: two_factor
|
||||
|
||||
- domain: secure.example.com
|
||||
policy: one_factor
|
||||
## Network based rule, if not provided any network matches.
|
||||
|
@ -397,21 +409,21 @@ access_control:
|
|||
## Rules applied to 'dev' group
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/groups/dev/.*$"
|
||||
- '^/groups/dev/.*$'
|
||||
subject: "group:dev"
|
||||
policy: two_factor
|
||||
|
||||
## Rules applied to user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/john/.*$"
|
||||
- '^/users/john/.*$'
|
||||
subject: "user:john"
|
||||
policy: two_factor
|
||||
|
||||
## Rules applied to user 'harry'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/harry/.*$"
|
||||
- '^/users/harry/.*$'
|
||||
subject: "user:harry"
|
||||
policy: two_factor
|
||||
|
||||
|
@ -421,7 +433,7 @@ access_control:
|
|||
policy: two_factor
|
||||
- domain: "dev.example.com"
|
||||
resources:
|
||||
- "^/users/bob/.*$"
|
||||
- '^/users/bob/.*$'
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/mail"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
@ -137,3 +138,37 @@ func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType {
|
|||
return duration, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToRegexpFunc decodes a string into a *regexp.Regexp or regexp.Regexp.
|
||||
func StringToRegexpFunc() mapstructure.DecodeHookFuncType {
|
||||
return func(f reflect.Type, t reflect.Type, data interface{}) (value interface{}, err error) {
|
||||
var ptr bool
|
||||
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
ptr = t.Kind() == reflect.Ptr
|
||||
|
||||
typeRegexp := reflect.TypeOf(regexp.Regexp{})
|
||||
|
||||
if ptr && t.Elem() != typeRegexp {
|
||||
return data, nil
|
||||
} else if !ptr && t != typeRegexp {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
regexStr := data.(string)
|
||||
|
||||
pattern, err := regexp.Compile(regexStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse '%s' as regexp: %w", regexStr, err)
|
||||
}
|
||||
|
||||
if ptr {
|
||||
return pattern, nil
|
||||
}
|
||||
|
||||
return *pattern, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,12 @@ package configuration
|
|||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStringToURLHookFunc_ShouldNotParseStrings(t *testing.T) {
|
||||
|
@ -417,3 +419,121 @@ func TestToTimeDurationHookFunc_ShouldParse_FromZero(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, result)
|
||||
}
|
||||
|
||||
func TestStringToRegexpFunc(t *testing.T) {
|
||||
wantRegexp := func(regexpStr string) regexp.Regexp {
|
||||
pattern := regexp.MustCompile(regexpStr)
|
||||
|
||||
return *pattern
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
have interface{}
|
||||
want regexp.Regexp
|
||||
wantPtr *regexp.Regexp
|
||||
wantErr string
|
||||
wantGroupNames []string
|
||||
}{
|
||||
{
|
||||
desc: "should not parse regexp with open paren",
|
||||
have: "hello(test one two",
|
||||
wantErr: "could not parse 'hello(test one two' as regexp: error parsing regexp: missing closing ): `hello(test one two`",
|
||||
},
|
||||
{
|
||||
desc: "should parse valid regex",
|
||||
have: "^(api|admin)$",
|
||||
want: wantRegexp("^(api|admin)$"),
|
||||
wantPtr: regexp.MustCompile("^(api|admin)$"),
|
||||
},
|
||||
{
|
||||
desc: "should parse valid regex with named groups",
|
||||
have: "^(?P<area>api|admin)$",
|
||||
want: wantRegexp("^(?P<area>api|admin)$"),
|
||||
wantPtr: regexp.MustCompile("^(?P<area>api|admin)$"),
|
||||
wantGroupNames: []string{"area"},
|
||||
},
|
||||
}
|
||||
|
||||
hook := StringToRegexpFunc()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Run("non-ptr", func(t *testing.T) {
|
||||
result, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.want), tc.have)
|
||||
if tc.wantErr != "" {
|
||||
assert.EqualError(t, err, tc.wantErr)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, tc.want, result)
|
||||
|
||||
resultRegexp := result.(regexp.Regexp)
|
||||
|
||||
actualNames := resultRegexp.SubexpNames()
|
||||
var names []string
|
||||
|
||||
for _, name := range actualNames {
|
||||
if name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tc.wantGroupNames) != 0 {
|
||||
t.Run("must_have_all_expected_subexp_group_names", func(t *testing.T) {
|
||||
for _, name := range tc.wantGroupNames {
|
||||
assert.Contains(t, names, name)
|
||||
}
|
||||
})
|
||||
t.Run("must_not_have_unexpected_subexp_group_names", func(t *testing.T) {
|
||||
for _, name := range names {
|
||||
assert.Contains(t, tc.wantGroupNames, name)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
t.Run("must_have_no_subexp_group_names", func(t *testing.T) {
|
||||
assert.Len(t, names, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("ptr", func(t *testing.T) {
|
||||
result, err := hook(reflect.TypeOf(tc.have), reflect.TypeOf(tc.wantPtr), tc.have)
|
||||
if tc.wantErr != "" {
|
||||
assert.EqualError(t, err, tc.wantErr)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.wantPtr, result)
|
||||
|
||||
resultRegexp := result.(*regexp.Regexp)
|
||||
|
||||
actualNames := resultRegexp.SubexpNames()
|
||||
var names []string
|
||||
|
||||
for _, name := range actualNames {
|
||||
if name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tc.wantGroupNames) != 0 {
|
||||
t.Run("must_have_all_expected_names", func(t *testing.T) {
|
||||
for _, name := range tc.wantGroupNames {
|
||||
assert.Contains(t, names, name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("must_not_have_unexpected_names", func(t *testing.T) {
|
||||
for _, name := range names {
|
||||
assert.Contains(t, tc.wantGroupNames, name)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
assert.Len(t, names, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o inte
|
|||
StringToMailAddressHookFunc(),
|
||||
ToTimeDurationHookFunc(),
|
||||
StringToURLHookFunc(),
|
||||
StringToRegexpFunc(),
|
||||
),
|
||||
Metadata: nil,
|
||||
Result: o,
|
||||
|
|
|
@ -298,6 +298,54 @@ func TestShouldDecodeSMTPSenderWithName(t *testing.T) {
|
|||
assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMeDuration)
|
||||
}
|
||||
|
||||
func TestShouldParseRegex(t *testing.T) {
|
||||
testReset()
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_domain_regex.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
|
||||
|
||||
assert.Len(t, val.Errors(), 0)
|
||||
assert.Len(t, val.Warnings(), 0)
|
||||
|
||||
validator.ValidateRules(config, val)
|
||||
|
||||
assert.Len(t, val.Errors(), 0)
|
||||
assert.Len(t, val.Warnings(), 0)
|
||||
|
||||
assert.Len(t, config.AccessControl.Rules[0].DomainsRegex[0].SubexpNames(), 2)
|
||||
assert.Equal(t, "", config.AccessControl.Rules[0].DomainsRegex[0].SubexpNames()[0])
|
||||
assert.Equal(t, "", config.AccessControl.Rules[0].DomainsRegex[0].SubexpNames()[1])
|
||||
|
||||
assert.Len(t, config.AccessControl.Rules[1].DomainsRegex[0].SubexpNames(), 2)
|
||||
assert.Equal(t, "", config.AccessControl.Rules[1].DomainsRegex[0].SubexpNames()[0])
|
||||
assert.Equal(t, "User", config.AccessControl.Rules[1].DomainsRegex[0].SubexpNames()[1])
|
||||
|
||||
assert.Len(t, config.AccessControl.Rules[2].DomainsRegex[0].SubexpNames(), 3)
|
||||
assert.Equal(t, "", config.AccessControl.Rules[2].DomainsRegex[0].SubexpNames()[0])
|
||||
assert.Equal(t, "User", config.AccessControl.Rules[2].DomainsRegex[0].SubexpNames()[1])
|
||||
assert.Equal(t, "Group", config.AccessControl.Rules[2].DomainsRegex[0].SubexpNames()[2])
|
||||
}
|
||||
|
||||
func TestShouldErrOnParseInvalidRegex(t *testing.T) {
|
||||
testReset()
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config_domain_bad_regex.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
|
||||
|
||||
require.Len(t, val.Errors(), 1)
|
||||
assert.Len(t, val.Warnings(), 0)
|
||||
|
||||
assert.EqualError(t, val.Errors()[0], "error occurred during unmarshalling configuration: 1 error(s) decoding:\n\n* error decoding 'access_control.rules[0].domain_regex[0]': could not parse '^\\K(public|public2).example.com$' as regexp: error parsing regexp: invalid escape sequence: `\\K`")
|
||||
}
|
||||
|
||||
func TestShouldNotReadConfigurationOnFSAccessDenied(t *testing.T) {
|
||||
if runtime.GOOS == constWindows {
|
||||
t.Skip("skipping test due to being on windows")
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// AccessControlConfiguration represents the configuration related to ACLs.
|
||||
type AccessControlConfiguration struct {
|
||||
DefaultPolicy string `koanf:"default_policy"`
|
||||
|
@ -7,19 +11,20 @@ type AccessControlConfiguration struct {
|
|||
Rules []ACLRule `koanf:"rules"`
|
||||
}
|
||||
|
||||
// ACLNetwork represents one ACL network group entry; "weak" coerces a single value into slice.
|
||||
// ACLNetwork represents one ACL network group entry.
|
||||
type ACLNetwork struct {
|
||||
Name string `koanf:"name"`
|
||||
Networks []string `koanf:"networks"`
|
||||
}
|
||||
|
||||
// ACLRule represents one ACL rule entry; "weak" coerces a single value into slice.
|
||||
// ACLRule represents one ACL rule entry.
|
||||
type ACLRule struct {
|
||||
Domains []string `koanf:"domain"`
|
||||
DomainsRegex []regexp.Regexp `koanf:"domain_regex"`
|
||||
Policy string `koanf:"policy"`
|
||||
Subjects [][]string `koanf:"subject"`
|
||||
Networks []string `koanf:"networks"`
|
||||
Resources []string `koanf:"resources"`
|
||||
Resources []regexp.Regexp `koanf:"resources"`
|
||||
Methods []string `koanf:"methods"`
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
---
|
||||
default_redirection_url: https://home.example.com:8080/
|
||||
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 9091
|
||||
|
||||
log:
|
||||
level: debug
|
||||
|
||||
totp:
|
||||
issuer: authelia.com
|
||||
|
||||
duo_api:
|
||||
hostname: api-123456789.example.com
|
||||
integration_key: ABCDEF
|
||||
|
||||
authentication_backend:
|
||||
ldap:
|
||||
url: ldap://127.0.0.1
|
||||
base_dn: dc=example,dc=com
|
||||
username_attribute: uid
|
||||
additional_users_dn: ou=users
|
||||
users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
|
||||
additional_groups_dn: ou=groups
|
||||
groups_filter: (&(member={dn})(objectClass=groupOfNames))
|
||||
group_name_attribute: cn
|
||||
mail_attribute: mail
|
||||
user: cn=admin,dc=example,dc=com
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
|
||||
rules:
|
||||
# Rules applied to everyone
|
||||
- domain_regex: ^\K(public|public2).example.com$
|
||||
policy: bypass
|
||||
|
||||
- domain: secure.example.com
|
||||
policy: one_factor
|
||||
# Network based rule, if not provided any network matches.
|
||||
networks:
|
||||
- 192.168.1.0/24
|
||||
- domain: secure.example.com
|
||||
policy: two_factor
|
||||
|
||||
- domain: [singlefactor.example.com, onefactor.example.com]
|
||||
policy: one_factor
|
||||
|
||||
# Rules applied to 'admins' group
|
||||
- domain: "mx2.mail.example.com"
|
||||
subject: "group:admins"
|
||||
policy: deny
|
||||
- domain: "*.example.com"
|
||||
subject: "group:admins"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to 'dev' group
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/groups/dev/.*$"
|
||||
subject: "group:dev"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/john/.*$"
|
||||
subject: "user:john"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to 'dev' group and user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/deny-all.*$"
|
||||
subject: ["group:dev", "user:john"]
|
||||
policy: deny
|
||||
|
||||
# Rules applied to user 'harry'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/harry/.*$"
|
||||
subject: "user:harry"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to user 'bob'
|
||||
- domain: "*.mail.example.com"
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
- domain: "dev.example.com"
|
||||
resources:
|
||||
- "^/users/bob/.*$"
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
session:
|
||||
name: authelia_session
|
||||
expiration: 3600000 # 1 hour
|
||||
inactivity: 300000 # 5 minutes
|
||||
domain: example.com
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
high_availability:
|
||||
sentinel_name: test
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 120
|
||||
ban_time: 300
|
||||
|
||||
storage:
|
||||
mysql:
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
database: authelia
|
||||
username: authelia
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
username: test
|
||||
host: 127.0.0.1
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
...
|
|
@ -0,0 +1,132 @@
|
|||
---
|
||||
default_redirection_url: https://home.example.com:8080/
|
||||
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 9091
|
||||
|
||||
log:
|
||||
level: debug
|
||||
|
||||
totp:
|
||||
issuer: authelia.com
|
||||
|
||||
duo_api:
|
||||
hostname: api-123456789.example.com
|
||||
integration_key: ABCDEF
|
||||
|
||||
authentication_backend:
|
||||
ldap:
|
||||
url: ldap://127.0.0.1
|
||||
base_dn: dc=example,dc=com
|
||||
username_attribute: uid
|
||||
additional_users_dn: ou=users
|
||||
users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user))
|
||||
additional_groups_dn: ou=groups
|
||||
groups_filter: (&(member={dn})(objectClass=groupOfNames))
|
||||
group_name_attribute: cn
|
||||
mail_attribute: mail
|
||||
user: cn=admin,dc=example,dc=com
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
|
||||
rules:
|
||||
# Rules applied to everyone
|
||||
- domain_regex: ^(public|public2).example.com$
|
||||
policy: bypass
|
||||
|
||||
- domain_regex: ^portfolio-(?P<User>[a-zA-Z0-9]+).example.com$
|
||||
policy: one_factor
|
||||
|
||||
- domain_regex: ^portfolio-(?P<User>[a-zA-Z0-9]+)-(?P<Group>[a-zA-Z0-9]+).example.com$
|
||||
policy: one_factor
|
||||
|
||||
- domain: secure.example.com
|
||||
policy: one_factor
|
||||
# Network based rule, if not provided any network matches.
|
||||
networks:
|
||||
- 192.168.1.0/24
|
||||
- domain: secure.example.com
|
||||
policy: two_factor
|
||||
|
||||
- domain: [singlefactor.example.com, onefactor.example.com]
|
||||
policy: one_factor
|
||||
|
||||
# Rules applied to 'admins' group
|
||||
- domain: "mx2.mail.example.com"
|
||||
subject: "group:admins"
|
||||
policy: deny
|
||||
- domain: "*.example.com"
|
||||
subject: "group:admins"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to 'dev' group
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/groups/dev/.*$"
|
||||
subject: "group:dev"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/john/.*$"
|
||||
subject: "user:john"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to 'dev' group and user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/deny-all.*$"
|
||||
subject: ["group:dev", "user:john"]
|
||||
policy: deny
|
||||
|
||||
# Rules applied to user 'harry'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- "^/users/harry/.*$"
|
||||
subject: "user:harry"
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to user 'bob'
|
||||
- domain: "*.mail.example.com"
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
- domain: "dev.example.com"
|
||||
resources:
|
||||
- "^/users/bob/.*$"
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
session:
|
||||
name: authelia_session
|
||||
expiration: 3600000 # 1 hour
|
||||
inactivity: 300000 # 5 minutes
|
||||
domain: example.com
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
high_availability:
|
||||
sentinel_name: test
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 120
|
||||
ban_time: 300
|
||||
|
||||
storage:
|
||||
mysql:
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
database: authelia
|
||||
username: authelia
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
username: test
|
||||
host: 127.0.0.1
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
...
|
|
@ -3,9 +3,9 @@ package validator
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
@ -15,12 +15,6 @@ func IsPolicyValid(policy string) (isValid bool) {
|
|||
return utils.IsStringInSlice(policy, validACLRulePolicies)
|
||||
}
|
||||
|
||||
// IsResourceValid check if a resource is valid.
|
||||
func IsResourceValid(resource string) (err error) {
|
||||
_, err = regexp.Compile(resource)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsSubjectValid check if a subject is valid.
|
||||
func IsSubjectValid(subject string) (isValid bool) {
|
||||
return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
|
||||
|
@ -95,7 +89,7 @@ func ValidateRules(config *schema.Configuration, validator *schema.StructValidat
|
|||
for i, rule := range config.AccessControl.Rules {
|
||||
rulePosition := i + 1
|
||||
|
||||
if len(rule.Domains) == 0 {
|
||||
if len(rule.Domains)+len(rule.DomainsRegex) == 0 {
|
||||
validator.Push(fmt.Errorf(errFmtAccessControlRuleNoDomains, ruleDescriptor(rulePosition, rule)))
|
||||
}
|
||||
|
||||
|
@ -105,15 +99,26 @@ func ValidateRules(config *schema.Configuration, validator *schema.StructValidat
|
|||
|
||||
validateNetworks(rulePosition, rule, config.AccessControl, validator)
|
||||
|
||||
validateResources(rulePosition, rule, validator)
|
||||
|
||||
validateSubjects(rulePosition, rule, validator)
|
||||
|
||||
validateMethods(rulePosition, rule, validator)
|
||||
|
||||
if rule.Policy == policyBypass && len(rule.Subjects) != 0 {
|
||||
if rule.Policy == policyBypass {
|
||||
validateBypass(rulePosition, rule, validator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateBypass(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) {
|
||||
if len(rule.Subjects) != 0 {
|
||||
validator.Push(fmt.Errorf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(rulePosition, rule)))
|
||||
}
|
||||
|
||||
for _, pattern := range rule.DomainsRegex {
|
||||
if utils.IsStringSliceContainsAny(authorization.IdentitySubexpNames, pattern.SubexpNames()) {
|
||||
validator.Push(fmt.Errorf(errAccessControlRuleBypassPolicyInvalidWithSubjectsWithGroupDomainRegex, ruleDescriptor(rulePosition, rule)))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,14 +132,6 @@ func validateNetworks(rulePosition int, rule schema.ACLRule, config schema.Acces
|
|||
}
|
||||
}
|
||||
|
||||
func validateResources(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) {
|
||||
for _, resource := range rule.Resources {
|
||||
if err := IsResourceValid(resource); err != nil {
|
||||
validator.Push(fmt.Errorf(errFmtAccessControlRuleResourceInvalid, ruleDescriptor(rulePosition, rule), resource, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateSubjects(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) {
|
||||
for _, subjectRule := range rule.Subjects {
|
||||
for _, subject := range subjectRule {
|
||||
|
|
|
@ -2,6 +2,7 @@ package validator
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -35,6 +36,31 @@ func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
|
|||
suite.Assert().False(suite.validator.HasErrors())
|
||||
}
|
||||
|
||||
func (suite *AccessControl) TestShouldValidateEitherDomainsOrDomainsRegex() {
|
||||
domainsRegex := regexp.MustCompile(`^abc.example.com$`)
|
||||
|
||||
suite.config.AccessControl.Rules = []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
{
|
||||
DomainsRegex: []regexp.Regexp{*domainsRegex},
|
||||
Policy: "bypass",
|
||||
},
|
||||
{
|
||||
Policy: "bypass",
|
||||
},
|
||||
}
|
||||
|
||||
ValidateRules(suite.config, suite.validator)
|
||||
|
||||
suite.Assert().False(suite.validator.HasWarnings())
|
||||
suite.Require().Len(suite.validator.Errors(), 1)
|
||||
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "access control: rule #3: rule is invalid: must have the option 'domain' or 'domain_regex' configured")
|
||||
}
|
||||
|
||||
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
|
||||
suite.config.AccessControl.DefaultPolicy = testInvalidPolicy
|
||||
|
||||
|
@ -99,9 +125,9 @@ func (suite *AccessControl) TestShouldRaiseErrorsWithEmptyRules() {
|
|||
suite.Assert().False(suite.validator.HasWarnings())
|
||||
suite.Require().Len(suite.validator.Errors(), 4)
|
||||
|
||||
suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1: rule is invalid: must have the option 'domain' configured")
|
||||
suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1: rule is invalid: must have the option 'domain' or 'domain_regex' configured")
|
||||
suite.Assert().EqualError(suite.validator.Errors()[1], "access control: rule #1: rule 'policy' option '' is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'")
|
||||
suite.Assert().EqualError(suite.validator.Errors()[2], "access control: rule #2: rule is invalid: must have the option 'domain' configured")
|
||||
suite.Assert().EqualError(suite.validator.Errors()[2], "access control: rule #2: rule is invalid: must have the option 'domain' or 'domain_regex' configured")
|
||||
suite.Assert().EqualError(suite.validator.Errors()[3], "access control: rule #2: rule 'policy' option 'wrong' is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'")
|
||||
}
|
||||
|
||||
|
@ -155,23 +181,6 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() {
|
|||
suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'methods' option 'HOP' is invalid: must be one of 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK'")
|
||||
}
|
||||
|
||||
func (suite *AccessControl) TestShouldRaiseErrorInvalidResource() {
|
||||
suite.config.AccessControl.Rules = []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"public.example.com"},
|
||||
Policy: "bypass",
|
||||
Resources: []string{"^/(api.*"},
|
||||
},
|
||||
}
|
||||
|
||||
ValidateRules(suite.config, suite.validator)
|
||||
|
||||
suite.Assert().False(suite.validator.HasWarnings())
|
||||
suite.Require().Len(suite.validator.Errors(), 1)
|
||||
|
||||
suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'resources' option '^/(api.*' is invalid: error parsing regexp: missing closing ): `^/(api.*`")
|
||||
}
|
||||
|
||||
func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
|
||||
domains := []string{"public.example.com"}
|
||||
subjects := [][]string{{"invalid"}}
|
||||
|
|
|
@ -169,16 +169,17 @@ const (
|
|||
errFmtAccessControlWarnNoRulesDefaultPolicy = "access control: no rules have been specified so the " +
|
||||
"'default_policy' of '%s' is going to be applied to all requests"
|
||||
errFmtAccessControlRuleNoDomains = "access control: rule %s: rule is invalid: must have the option " +
|
||||
"'domain' configured"
|
||||
"'domain' or 'domain_regex' configured"
|
||||
errFmtAccessControlRuleInvalidPolicy = "access control: rule %s: rule 'policy' option '%s' " +
|
||||
"is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'"
|
||||
errAccessControlRuleBypassPolicyInvalidWithSubjects = "access control: rule %s: 'policy' option 'bypass' is " +
|
||||
"not supported when 'subject' option is configured: see " +
|
||||
"https://www.authelia.com/docs/configuration/access-control.html#bypass"
|
||||
errAccessControlRuleBypassPolicyInvalidWithSubjectsWithGroupDomainRegex = "access control: rule %s: 'policy' option 'bypass' is " +
|
||||
"not supported when 'domain_regex' option contains the user or group named matches. For more information see: " +
|
||||
"https://www.authelia.com/docs/configuration/access-control.html#bypass-and-user-identity"
|
||||
errFmtAccessControlRuleNetworksInvalid = "access control: rule %s: the network '%s' is not a " +
|
||||
"valid Group Name, IP, or CIDR notation"
|
||||
errFmtAccessControlRuleResourceInvalid = "access control: rule %s: 'resources' option '%s' is " +
|
||||
"invalid: %w"
|
||||
errFmtAccessControlRuleSubjectInvalid = "access control: rule %s: 'subject' option '%s' is " +
|
||||
"invalid: must start with 'user:' or 'group:'"
|
||||
errFmtAccessControlRuleMethodInvalid = "access control: rule %s: 'methods' option '%s' is " +
|
||||
|
@ -327,6 +328,7 @@ var ValidKeys = []string{
|
|||
"access_control.networks[].networks",
|
||||
"access_control.rules",
|
||||
"access_control.rules[].domain",
|
||||
"access_control.rules[].domain_regex",
|
||||
"access_control.rules[].methods",
|
||||
"access_control.rules[].networks",
|
||||
"access_control.rules[].subject",
|
||||
|
|
Loading…
Reference in New Issue