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
|
- domain: public.example.com
|
||||||
policy: bypass
|
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
|
- domain: secure.example.com
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
## Network based rule, if not provided any network matches.
|
## Network based rule, if not provided any network matches.
|
||||||
|
@ -397,21 +409,21 @@ access_control:
|
||||||
## Rules applied to 'dev' group
|
## Rules applied to 'dev' group
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/groups/dev/.*$"
|
- '^/groups/dev/.*$'
|
||||||
subject: "group:dev"
|
subject: "group:dev"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
## Rules applied to user 'john'
|
## Rules applied to user 'john'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/users/john/.*$"
|
- '^/users/john/.*$'
|
||||||
subject: "user:john"
|
subject: "user:john"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
## Rules applied to user 'harry'
|
## Rules applied to user 'harry'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/users/harry/.*$"
|
- '^/users/harry/.*$'
|
||||||
subject: "user:harry"
|
subject: "user:harry"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
|
@ -421,7 +433,7 @@ access_control:
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
- domain: "dev.example.com"
|
- domain: "dev.example.com"
|
||||||
resources:
|
resources:
|
||||||
- "^/users/bob/.*$"
|
- '^/users/bob/.*$'
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ access_control:
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
- domain: public.example.com
|
- domain: public.example.com
|
||||||
|
domain_regex: "^\d+\\.public.example.com$"
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
|
@ -35,7 +36,7 @@ access_control:
|
||||||
- GET
|
- GET
|
||||||
- HEAD
|
- HEAD
|
||||||
resources:
|
resources:
|
||||||
- "^/api.*"
|
- '^/api.*'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
@ -96,6 +97,7 @@ A rule defines two primary things:
|
||||||
The criteria is broken into several parts:
|
The criteria is broken into several parts:
|
||||||
|
|
||||||
* [domain](#domain): domain or list of domains targeted by the request.
|
* [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.
|
* [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.
|
* [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.
|
* [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
|
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.
|
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
|
#### domain
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
|
@ -126,8 +117,12 @@ required: yes
|
||||||
{: .label .label-config .label-red }
|
{: .label .label-config .label-red }
|
||||||
</div>
|
</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
|
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.
|
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:
|
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"`.
|
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
|
* 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
|
example `{user}.example.com` would match `fred.example.com` if the user logged in was named `fred`. _**Warning:** this is
|
||||||
considering refactoring this to just be regex which would likely allow many additional possibilities.*
|
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
|
* 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
|
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
|
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
|
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
|
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
|
### subject
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(list(string))
|
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
|
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.*
|
[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
|
This criteria matches identifying characteristics about the subject. This is either user's name or the name of the
|
||||||
belongs to. This allows you to effectively control exactly what each user is authorized to access or to specifically
|
group a user belongs to. This allows you to effectively control exactly what each user is authorized to access or to
|
||||||
require two-factor authentication to specific users. Subjects are prefixed with either `user:` or `group:` to identify
|
specifically require two-factor authentication to specific users. Subjects are prefixed with either `user:` or `group:`
|
||||||
which part of the identity to check.
|
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
|
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.
|
`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
|
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.*
|
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
|
As this is a regex string you will either need to use single quotes or need to double-escape certain portions of the
|
||||||
with escaping the expressions. Failure to do so may prevent Authelia from starting. It's technically optional but will
|
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
|
||||||
likely save you a lot of time if you do it for all resource rules.
|
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:
|
Examples:
|
||||||
|
|
||||||
|
@ -370,16 +430,13 @@ access_control:
|
||||||
- domain: app.example.com
|
- domain: app.example.com
|
||||||
policy: bypass
|
policy: bypass
|
||||||
resources:
|
resources:
|
||||||
- "^/api([/?].*)?$"
|
- '^/api([/?].*)?$'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Policies
|
## Policies
|
||||||
|
|
||||||
With **Authelia** you can define a list of rules that are going to be evaluated in
|
The policy of the first matching rule in the configured list decides the policy applied to the request, if no rule
|
||||||
sequential order when authorization is delegated to Authelia.
|
matches the request the [default_policy](#default_policy) is applied.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### deny
|
### 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
|
that includes a [subject](#subject) restriction because the minimum authentication level required to obtain information
|
||||||
about the subject is [one_factor](#one_factor).
|
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
|
### one_factor
|
||||||
|
|
||||||
This policy requires the user at minimum complete 1FA successfully (username and password). This means if they have
|
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
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/groups/dev/.*$"
|
- '^/groups/dev/.*$'
|
||||||
subject: "group:dev"
|
subject: "group:dev"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/users/john/.*$"
|
- '^/users/john/.*$'
|
||||||
subject:
|
subject:
|
||||||
- ["group:dev", "user:john"]
|
- ["group:dev", "user:john"]
|
||||||
- "group:admins"
|
- "group:admins"
|
||||||
|
|
|
@ -2,11 +2,35 @@ package authorization
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"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.
|
// AccessControlDomain represents an ACL domain.
|
||||||
type AccessControlDomain struct {
|
type AccessControlDomain struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -16,17 +40,88 @@ type AccessControlDomain struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMatch returns true if the ACL domain matches the object domain.
|
// 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 {
|
switch {
|
||||||
case acd.Wildcard:
|
case acl.Wildcard:
|
||||||
return strings.HasSuffix(object.Domain, acd.Name)
|
return strings.HasSuffix(object.Domain, acl.Name)
|
||||||
case acd.UserWildcard:
|
case acl.UserWildcard:
|
||||||
return object.Domain == fmt.Sprintf("%s.%s", subject.Username, acd.Name)
|
return object.Domain == fmt.Sprintf("%s.%s", subject.Username, acl.Name)
|
||||||
case acd.GroupWildcard:
|
case acl.GroupWildcard:
|
||||||
prefix, suffix := domainToPrefixSuffix(object.Domain)
|
prefix, suffix := domainToPrefixSuffix(object.Domain)
|
||||||
|
|
||||||
return suffix == acd.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
return suffix == acl.Name && utils.IsStringInSliceFold(prefix, subject.Groups)
|
||||||
default:
|
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.
|
// AccessControlResource represents an ACL resource.
|
||||||
type AccessControlResource struct {
|
type AccessControlResource struct {
|
||||||
Pattern *regexp.Regexp
|
Pattern regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMatch returns true if the ACL resource match the object path.
|
// 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 {
|
func NewAccessControlRule(pos int, rule schema.ACLRule, networksMap map[string][]*net.IPNet, networksCacheMap map[string]*net.IPNet) *AccessControlRule {
|
||||||
return &AccessControlRule{
|
return &AccessControlRule{
|
||||||
Position: pos,
|
Position: pos,
|
||||||
Domains: schemaDomainsToACL(rule.Domains),
|
Domains: schemaDomainsToACL(rule.Domains, rule.DomainsRegex),
|
||||||
Resources: schemaResourcesToACL(rule.Resources),
|
Resources: schemaResourcesToACL(rule.Resources),
|
||||||
Methods: schemaMethodsToACL(rule.Methods),
|
Methods: schemaMethodsToACL(rule.Methods),
|
||||||
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
|
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.
|
// AccessControlRule controls and represents an ACL internally.
|
||||||
type AccessControlRule struct {
|
type AccessControlRule struct {
|
||||||
Position int
|
Position int
|
||||||
Domains []AccessControlDomain
|
Domains []SubjectObjectMatcher
|
||||||
Resources []AccessControlResource
|
Resources []AccessControlResource
|
||||||
Methods []string
|
Methods []string
|
||||||
Networks []*net.IPNet
|
Networks []*net.IPNet
|
||||||
|
|
|
@ -4,14 +4,9 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"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.
|
// AccessControlSubjects represents an ACL subject.
|
||||||
type AccessControlSubjects struct {
|
type AccessControlSubjects struct {
|
||||||
Subjects []AccessControlSubject
|
Subjects []SubjectMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSubject appends to the AccessControlSubjects based on a subject rule string.
|
// AddSubject appends to the AccessControlSubjects based on a subject rule string.
|
||||||
|
|
|
@ -3,6 +3,7 @@ package authorization
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -230,6 +231,118 @@ func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
|
||||||
tester.CheckAuthorizations(s.T(), John, "https://public.example.com/", "GET", TwoFactor)
|
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() {
|
func (s *AuthorizerSuite) TestShouldCheckUserMatching() {
|
||||||
tester := NewAuthorizerBuilder().
|
tester := NewAuthorizerBuilder().
|
||||||
WithDefaultPolicy(deny).
|
WithDefaultPolicy(deny).
|
||||||
|
@ -367,17 +480,25 @@ func (s *AuthorizerSuite) TestShouldCheckMethodMatching() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestShouldCheckResourceMatching() {
|
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().
|
tester := NewAuthorizerBuilder().
|
||||||
WithDefaultPolicy(deny).
|
WithDefaultPolicy(deny).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"resource.example.com"},
|
Domains: []string{"resource.example.com"},
|
||||||
Policy: bypass,
|
Policy: bypass,
|
||||||
Resources: []string{"^/bypass/[a-z]+$", "^/$", "embedded"},
|
Resources: createSliceRegexRule(s.T(), []string{"^/bypass/[a-z]+$", "^/$", "embedded"}),
|
||||||
}).
|
}).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"resource.example.com"},
|
Domains: []string{"resource.example.com"},
|
||||||
Policy: oneFactor,
|
Policy: oneFactor,
|
||||||
Resources: []string{"^/one_factor/[a-z]+$"},
|
Resources: createSliceRegexRule(s.T(), []string{"^/one_factor/[a-z]+$"}),
|
||||||
}).
|
}).
|
||||||
Build()
|
Build()
|
||||||
|
|
||||||
|
@ -424,17 +545,25 @@ func (s *AuthorizerSuite) TestShouldMatchAnyDomainIfBlank() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
|
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().
|
tester := NewAuthorizerBuilder().
|
||||||
WithDefaultPolicy(deny).
|
WithDefaultPolicy(deny).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"public.example.com"},
|
Domains: []string{"public.example.com"},
|
||||||
Resources: []string{"^/admin/.*$"},
|
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||||
Subjects: [][]string{{"group:admins"}},
|
Subjects: [][]string{{"group:admins"}},
|
||||||
Policy: oneFactor,
|
Policy: oneFactor,
|
||||||
}).
|
}).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"public.example.com"},
|
Domains: []string{"public.example.com"},
|
||||||
Resources: []string{"^/admin/.*$"},
|
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||||
Policy: deny,
|
Policy: deny,
|
||||||
}).
|
}).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
|
@ -443,13 +572,13 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
|
||||||
}).
|
}).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"public2.example.com"},
|
Domains: []string{"public2.example.com"},
|
||||||
Resources: []string{"^/admin/.*$"},
|
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||||
Subjects: [][]string{{"group:admins"}},
|
Subjects: [][]string{{"group:admins"}},
|
||||||
Policy: bypass,
|
Policy: bypass,
|
||||||
}).
|
}).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
Domains: []string{"public2.example.com"},
|
Domains: []string{"public2.example.com"},
|
||||||
Resources: []string{"^/admin/.*$"},
|
Resources: createSliceRegexRule(s.T(), []string{"^/admin/.*$"}),
|
||||||
Policy: deny,
|
Policy: deny,
|
||||||
}).
|
}).
|
||||||
WithRule(schema.ACLRule{
|
WithRule(schema.ACLRule{
|
||||||
|
|
|
@ -14,12 +14,26 @@ const (
|
||||||
Denied Level = iota
|
Denied Level = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
const userPrefix = "user:"
|
const (
|
||||||
const groupPrefix = "group:"
|
prefixUser = "user:"
|
||||||
|
prefixGroup = "group:"
|
||||||
|
)
|
||||||
|
|
||||||
const bypass = "bypass"
|
const (
|
||||||
const oneFactor = "one_factor"
|
bypass = "bypass"
|
||||||
const twoFactor = "two_factor"
|
oneFactor = "one_factor"
|
||||||
const deny = "deny"
|
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)"
|
const traceFmtACLHitMiss = "ACL %s Position %d for subject %s and object %s (Method %s)"
|
||||||
|
|
|
@ -7,6 +7,17 @@ import (
|
||||||
"strings"
|
"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.
|
// Subject represents the identity of a user for the purposes of ACL matching.
|
||||||
type Subject struct {
|
type Subject struct {
|
||||||
Username string
|
Username string
|
||||||
|
|
|
@ -20,3 +20,16 @@ func TestShouldAppendQueryParamToURL(t *testing.T) {
|
||||||
assert.Equal(t, "/api?type=none", object.Path)
|
assert.Equal(t, "/api?type=none", object.Path)
|
||||||
assert.Equal(t, "https", object.Scheme)
|
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
|
return deny
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaSubjectToACLSubject(subjectRule string) (subject AccessControlSubject) {
|
func stringSliceToRegexpSlice(strings []string) (regexps []regexp.Regexp, err error) {
|
||||||
if strings.HasPrefix(subjectRule, userPrefix) {
|
for _, str := range strings {
|
||||||
user := strings.Trim(subjectRule[len(userPrefix):], " ")
|
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}
|
return AccessControlUser{Name: user}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(subjectRule, groupPrefix) {
|
if strings.HasPrefix(subjectRule, prefixGroup) {
|
||||||
group := strings.Trim(subjectRule[len(groupPrefix):], " ")
|
group := strings.Trim(subjectRule[len(prefixGroup):], " ")
|
||||||
|
|
||||||
return AccessControlGroup{Name: group}
|
return AccessControlGroup{Name: group}
|
||||||
}
|
}
|
||||||
|
@ -57,35 +70,21 @@ func schemaSubjectToACLSubject(subjectRule string) (subject AccessControlSubject
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaDomainsToACL(domainRules []string) (domains []AccessControlDomain) {
|
func schemaDomainsToACL(domainRules []string, domainRegexRules []regexp.Regexp) (domains []SubjectObjectMatcher) {
|
||||||
for _, domainRule := range domainRules {
|
for _, domainRule := range domainRules {
|
||||||
domain := AccessControlDomain{}
|
domains = append(domains, NewAccessControlDomain(domainRule))
|
||||||
|
|
||||||
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, domain)
|
for _, domainRegexRule := range domainRegexRules {
|
||||||
|
domains = append(domains, NewAccessControlDomainRegex(domainRegexRule))
|
||||||
}
|
}
|
||||||
|
|
||||||
return domains
|
return domains
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaResourcesToACL(resourceRules []string) (resources []AccessControlResource) {
|
func schemaResourcesToACL(resourceRules []regexp.Regexp) (resources []AccessControlResource) {
|
||||||
for _, resourceRule := range resourceRules {
|
for _, resourceRule := range resourceRules {
|
||||||
resources = append(resources, AccessControlResource{regexp.MustCompile(resourceRule)})
|
resources = append(resources, AccessControlResource{Pattern: resourceRule})
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources
|
return resources
|
||||||
|
|
|
@ -185,6 +185,9 @@ func TestShouldParseACLNetworks(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldReturnCorrectValidationLevel(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.NotAuthenticated, Bypass))
|
||||||
assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, Bypass))
|
assert.True(t, IsAuthLevelSufficient(authentication.OneFactor, Bypass))
|
||||||
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, Bypass))
|
assert.True(t, IsAuthLevelSufficient(authentication.TwoFactor, Bypass))
|
||||||
|
|
|
@ -366,6 +366,18 @@ access_control:
|
||||||
- domain: public.example.com
|
- domain: public.example.com
|
||||||
policy: bypass
|
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
|
- domain: secure.example.com
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
## Network based rule, if not provided any network matches.
|
## Network based rule, if not provided any network matches.
|
||||||
|
@ -397,21 +409,21 @@ access_control:
|
||||||
## Rules applied to 'dev' group
|
## Rules applied to 'dev' group
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/groups/dev/.*$"
|
- '^/groups/dev/.*$'
|
||||||
subject: "group:dev"
|
subject: "group:dev"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
## Rules applied to user 'john'
|
## Rules applied to user 'john'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/users/john/.*$"
|
- '^/users/john/.*$'
|
||||||
subject: "user:john"
|
subject: "user:john"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
## Rules applied to user 'harry'
|
## Rules applied to user 'harry'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
resources:
|
resources:
|
||||||
- "^/users/harry/.*$"
|
- '^/users/harry/.*$'
|
||||||
subject: "user:harry"
|
subject: "user:harry"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
|
@ -421,7 +433,7 @@ access_control:
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
- domain: "dev.example.com"
|
- domain: "dev.example.com"
|
||||||
resources:
|
resources:
|
||||||
- "^/users/bob/.*$"
|
- '^/users/bob/.*$'
|
||||||
subject: "user:bob"
|
subject: "user:bob"
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
@ -137,3 +138,37 @@ func ToTimeDurationHookFunc() mapstructure.DecodeHookFuncType {
|
||||||
return duration, nil
|
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 (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStringToURLHookFunc_ShouldNotParseStrings(t *testing.T) {
|
func TestStringToURLHookFunc_ShouldNotParseStrings(t *testing.T) {
|
||||||
|
@ -417,3 +419,121 @@ func TestToTimeDurationHookFunc_ShouldParse_FromZero(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &expected, result)
|
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(),
|
StringToMailAddressHookFunc(),
|
||||||
ToTimeDurationHookFunc(),
|
ToTimeDurationHookFunc(),
|
||||||
StringToURLHookFunc(),
|
StringToURLHookFunc(),
|
||||||
|
StringToRegexpFunc(),
|
||||||
),
|
),
|
||||||
Metadata: nil,
|
Metadata: nil,
|
||||||
Result: o,
|
Result: o,
|
||||||
|
|
|
@ -298,6 +298,54 @@ func TestShouldDecodeSMTPSenderWithName(t *testing.T) {
|
||||||
assert.Equal(t, schema.RememberMeDisabled, config.Session.RememberMeDuration)
|
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) {
|
func TestShouldNotReadConfigurationOnFSAccessDenied(t *testing.T) {
|
||||||
if runtime.GOOS == constWindows {
|
if runtime.GOOS == constWindows {
|
||||||
t.Skip("skipping test due to being on windows")
|
t.Skip("skipping test due to being on windows")
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
// AccessControlConfiguration represents the configuration related to ACLs.
|
// AccessControlConfiguration represents the configuration related to ACLs.
|
||||||
type AccessControlConfiguration struct {
|
type AccessControlConfiguration struct {
|
||||||
DefaultPolicy string `koanf:"default_policy"`
|
DefaultPolicy string `koanf:"default_policy"`
|
||||||
|
@ -7,19 +11,20 @@ type AccessControlConfiguration struct {
|
||||||
Rules []ACLRule `koanf:"rules"`
|
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 {
|
type ACLNetwork struct {
|
||||||
Name string `koanf:"name"`
|
Name string `koanf:"name"`
|
||||||
Networks []string `koanf:"networks"`
|
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 {
|
type ACLRule struct {
|
||||||
Domains []string `koanf:"domain"`
|
Domains []string `koanf:"domain"`
|
||||||
|
DomainsRegex []regexp.Regexp `koanf:"domain_regex"`
|
||||||
Policy string `koanf:"policy"`
|
Policy string `koanf:"policy"`
|
||||||
Subjects [][]string `koanf:"subject"`
|
Subjects [][]string `koanf:"subject"`
|
||||||
Networks []string `koanf:"networks"`
|
Networks []string `koanf:"networks"`
|
||||||
Resources []string `koanf:"resources"`
|
Resources []regexp.Regexp `koanf:"resources"`
|
||||||
Methods []string `koanf:"methods"`
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
)
|
)
|
||||||
|
@ -15,12 +15,6 @@ func IsPolicyValid(policy string) (isValid bool) {
|
||||||
return utils.IsStringInSlice(policy, validACLRulePolicies)
|
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.
|
// IsSubjectValid check if a subject is valid.
|
||||||
func IsSubjectValid(subject string) (isValid bool) {
|
func IsSubjectValid(subject string) (isValid bool) {
|
||||||
return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
|
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 {
|
for i, rule := range config.AccessControl.Rules {
|
||||||
rulePosition := i + 1
|
rulePosition := i + 1
|
||||||
|
|
||||||
if len(rule.Domains) == 0 {
|
if len(rule.Domains)+len(rule.DomainsRegex) == 0 {
|
||||||
validator.Push(fmt.Errorf(errFmtAccessControlRuleNoDomains, ruleDescriptor(rulePosition, rule)))
|
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)
|
validateNetworks(rulePosition, rule, config.AccessControl, validator)
|
||||||
|
|
||||||
validateResources(rulePosition, rule, validator)
|
|
||||||
|
|
||||||
validateSubjects(rulePosition, rule, validator)
|
validateSubjects(rulePosition, rule, validator)
|
||||||
|
|
||||||
validateMethods(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)))
|
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) {
|
func validateSubjects(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) {
|
||||||
for _, subjectRule := range rule.Subjects {
|
for _, subjectRule := range rule.Subjects {
|
||||||
for _, subject := range subjectRule {
|
for _, subject := range subjectRule {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -35,6 +36,31 @@ func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
|
||||||
suite.Assert().False(suite.validator.HasErrors())
|
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() {
|
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
|
||||||
suite.config.AccessControl.DefaultPolicy = testInvalidPolicy
|
suite.config.AccessControl.DefaultPolicy = testInvalidPolicy
|
||||||
|
|
||||||
|
@ -99,9 +125,9 @@ func (suite *AccessControl) TestShouldRaiseErrorsWithEmptyRules() {
|
||||||
suite.Assert().False(suite.validator.HasWarnings())
|
suite.Assert().False(suite.validator.HasWarnings())
|
||||||
suite.Require().Len(suite.validator.Errors(), 4)
|
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()[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'")
|
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'")
|
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() {
|
func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
|
||||||
domains := []string{"public.example.com"}
|
domains := []string{"public.example.com"}
|
||||||
subjects := [][]string{{"invalid"}}
|
subjects := [][]string{{"invalid"}}
|
||||||
|
|
|
@ -169,16 +169,17 @@ const (
|
||||||
errFmtAccessControlWarnNoRulesDefaultPolicy = "access control: no rules have been specified so the " +
|
errFmtAccessControlWarnNoRulesDefaultPolicy = "access control: no rules have been specified so the " +
|
||||||
"'default_policy' of '%s' is going to be applied to all requests"
|
"'default_policy' of '%s' is going to be applied to all requests"
|
||||||
errFmtAccessControlRuleNoDomains = "access control: rule %s: rule is invalid: must have the option " +
|
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' " +
|
errFmtAccessControlRuleInvalidPolicy = "access control: rule %s: rule 'policy' option '%s' " +
|
||||||
"is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'"
|
"is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'"
|
||||||
errAccessControlRuleBypassPolicyInvalidWithSubjects = "access control: rule %s: 'policy' option 'bypass' is " +
|
errAccessControlRuleBypassPolicyInvalidWithSubjects = "access control: rule %s: 'policy' option 'bypass' is " +
|
||||||
"not supported when 'subject' option is configured: see " +
|
"not supported when 'subject' option is configured: see " +
|
||||||
"https://www.authelia.com/docs/configuration/access-control.html#bypass"
|
"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 " +
|
errFmtAccessControlRuleNetworksInvalid = "access control: rule %s: the network '%s' is not a " +
|
||||||
"valid Group Name, IP, or CIDR notation"
|
"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 " +
|
errFmtAccessControlRuleSubjectInvalid = "access control: rule %s: 'subject' option '%s' is " +
|
||||||
"invalid: must start with 'user:' or 'group:'"
|
"invalid: must start with 'user:' or 'group:'"
|
||||||
errFmtAccessControlRuleMethodInvalid = "access control: rule %s: 'methods' option '%s' is " +
|
errFmtAccessControlRuleMethodInvalid = "access control: rule %s: 'methods' option '%s' is " +
|
||||||
|
@ -327,6 +328,7 @@ var ValidKeys = []string{
|
||||||
"access_control.networks[].networks",
|
"access_control.networks[].networks",
|
||||||
"access_control.rules",
|
"access_control.rules",
|
||||||
"access_control.rules[].domain",
|
"access_control.rules[].domain",
|
||||||
|
"access_control.rules[].domain_regex",
|
||||||
"access_control.rules[].methods",
|
"access_control.rules[].methods",
|
||||||
"access_control.rules[].networks",
|
"access_control.rules[].networks",
|
||||||
"access_control.rules[].subject",
|
"access_control.rules[].subject",
|
||||||
|
|
Loading…
Reference in New Issue