feat(authorization): query parameter filtering (#3990)
This allows for advanced filtering of the query parameters in ACL's. Closes #2708pull/3671/head^2
parent
46ae5b2bf3
commit
52102eea8c
|
@ -201,9 +201,18 @@ func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
func readTags(prefix string, t reflect.Type) (tags []string) {
|
func readTags(prefix string, t reflect.Type) (tags []string) {
|
||||||
tags = make([]string, 0)
|
tags = make([]string, 0)
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, "", true), t.Elem())...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
field := t.Field(i)
|
field := t.Field(i)
|
||||||
|
|
||||||
|
@ -223,13 +232,16 @@ func readTags(prefix string, t reflect.Type) (tags []string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if field.Type.Elem().Kind() == reflect.Struct {
|
switch field.Type.Elem().Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
if !containsType(field.Type.Elem(), decodedTypes) {
|
if !containsType(field.Type.Elem(), decodedTypes) {
|
||||||
tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false))
|
tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false))
|
||||||
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
|
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...)
|
||||||
}
|
}
|
||||||
case reflect.Ptr:
|
case reflect.Ptr:
|
||||||
switch field.Type.Elem().Kind() {
|
switch field.Type.Elem().Kind() {
|
||||||
|
@ -268,6 +280,10 @@ func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if slice {
|
if slice {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Sprintf("%s[]", prefix)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s.%s[]", prefix, nameParts[0])
|
return fmt.Sprintf("%s.%s[]", prefix, nameParts[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,17 @@ access_control:
|
||||||
- HEAD
|
- HEAD
|
||||||
resources:
|
resources:
|
||||||
- '^/api.*'
|
- '^/api.*'
|
||||||
|
query:
|
||||||
|
- - operator: 'present'
|
||||||
|
key: 'secure'
|
||||||
|
- operator: 'absent'
|
||||||
|
key: 'insecure'
|
||||||
|
- - operator: 'pattern'
|
||||||
|
key: 'token'
|
||||||
|
value: '^(abc123|zyx789)$'
|
||||||
|
- operator: 'not pattern'
|
||||||
|
key: 'random'
|
||||||
|
value: '^(1|2)$'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
@ -416,6 +427,61 @@ access_control:
|
||||||
- '^/api([/?].*)?$'
|
- '^/api([/?].*)?$'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### query
|
||||||
|
|
||||||
|
{{< confkey type="list(list(object))" required="no" >}}
|
||||||
|
|
||||||
|
The query criteria is an advanced criteria which can allow configuration of rules that match specific query argument
|
||||||
|
keys against various rules. It's recommended to use [resources](#resources) rules instead for basic needs.
|
||||||
|
|
||||||
|
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.
|
||||||
|
Additionally each level of these lists does not have to be explicitly defined.
|
||||||
|
|
||||||
|
##### key
|
||||||
|
|
||||||
|
{{< confkey type="string" required="yes" >}}
|
||||||
|
|
||||||
|
The query argument key to check.
|
||||||
|
|
||||||
|
##### value
|
||||||
|
|
||||||
|
{{< confkey type="string" required="situational" >}}
|
||||||
|
|
||||||
|
The value to match against. This is required unless the operator is `absent` or `present`. It's recommended this value
|
||||||
|
is always quoted as per the examples.
|
||||||
|
|
||||||
|
##### operator
|
||||||
|
|
||||||
|
{{< confkey type="string" required="situational" >}}
|
||||||
|
|
||||||
|
The rule operator for this rule. Valid operators can be found in the
|
||||||
|
[Rule Operators](../../reference/guides/rule-operators.md#operators) reference guide.
|
||||||
|
|
||||||
|
If [key](#key) and [value](#value) are specified this defaults to `equal`, otherwise if [key](#key) is specified it
|
||||||
|
defaults to `present`.
|
||||||
|
|
||||||
|
|
||||||
|
##### Examples
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
access_control:
|
||||||
|
rules:
|
||||||
|
- domain: app.example.com
|
||||||
|
policy: bypass
|
||||||
|
query:
|
||||||
|
- - operator: 'present'
|
||||||
|
key: 'secure'
|
||||||
|
- operator: 'absent'
|
||||||
|
key: 'insecure'
|
||||||
|
- - operator: 'pattern'
|
||||||
|
key: 'token'
|
||||||
|
value: '^(abc123|zyx789)$'
|
||||||
|
- operator: 'not pattern'
|
||||||
|
key: 'random'
|
||||||
|
value: '^(1|2)$'
|
||||||
|
```
|
||||||
|
|
||||||
## Policies
|
## Policies
|
||||||
|
|
||||||
The policy of the first matching rule in the configured list decides the policy applied to the request, if no rule
|
The policy of the first matching rule in the configured list decides the policy applied to the request, if no rule
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Synology DSM"
|
title: "Synology DSM"
|
||||||
description: "Integrating Synology DSM with the Authelia OpenID Connect Provider."
|
description: "Integrating Synology DSM with the Authelia OpenID Connect Provider."
|
||||||
lead: ""
|
lead: ""
|
||||||
date: 2022-06-15T17:51:47+10:00
|
date: 2022-10-18T21:22:13+11:00
|
||||||
draft: false
|
draft: false
|
||||||
images: []
|
images: []
|
||||||
menu:
|
menu:
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
---
|
||||||
|
title: "Access Control Rule Guide"
|
||||||
|
description: "A reference guide on access control rule operators"
|
||||||
|
lead: "This section contains a reference guide on access control rule operators."
|
||||||
|
date: 2022-09-09T15:44:23+10:00
|
||||||
|
draft: false
|
||||||
|
images: []
|
||||||
|
menu:
|
||||||
|
reference:
|
||||||
|
parent: "guides"
|
||||||
|
weight: 220
|
||||||
|
toc: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operators
|
||||||
|
|
||||||
|
Rule operators are effectively words which alter the behaviour of particular access control rules. The following table
|
||||||
|
is a guide on their use.
|
||||||
|
|
||||||
|
| Operator | Effect |
|
||||||
|
|:-------------:|:--------------------------------------------------------------:|
|
||||||
|
| `equal` | Matches when the item value is equal to the provided value |
|
||||||
|
| `not equal` | Matches when the item value is not equal to the provided value |
|
||||||
|
| `present` | Matches when the item is present with any value |
|
||||||
|
| `absent` | Matches when the item is not present at all |
|
||||||
|
| `pattern` | Matches when the item matches the regex pattern |
|
||||||
|
| `not pattern` | Matches when the item doesn't match the regex pattern |
|
||||||
|
|
||||||
|
|
||||||
|
## Multi-level Logical Criteria
|
||||||
|
|
||||||
|
Criteria which is described as multi-level logical criteria indicates that it is a list of lists. The first level i.e.
|
||||||
|
the list least indented to the right will be referred to the `OR-list`, and the list most indented to the right will be
|
||||||
|
referred to the `AND-list`.
|
||||||
|
|
||||||
|
The OR-list matches if any of the criteria from it's AND-list's matches; in other words, a *__logical OR__*. The
|
||||||
|
AND-list matches if all of it's criteria matches the given request; in other words, a *__logical AND__*.
|
||||||
|
|
||||||
|
In addition to these rules, if the AND-list only needs one item, it can be represented without the second level.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### List of Lists
|
||||||
|
|
||||||
|
The following examples show various abstract examples to express a rule that matches either c, or a AND b;
|
||||||
|
i.e `(a AND b) OR (c)`. In relation to access control rules all of these should be treated the same. This format should
|
||||||
|
not be used for the configuration item type `list(list(object))`, see [List of List Objects](#list-of-list-objects)
|
||||||
|
instead.
|
||||||
|
|
||||||
|
##### Fully Expressed
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- - 'a'
|
||||||
|
- 'b'
|
||||||
|
- - 'c'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Omitted Level
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- - 'a'
|
||||||
|
- 'b'
|
||||||
|
- 'c'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Compact
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- ['a', 'b']
|
||||||
|
- ['c']
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Compact with Omitted Level
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- ['a', 'b']
|
||||||
|
- 'c'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Super Compact
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule: [['a', 'b'], ['c']]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List of List Objects
|
||||||
|
|
||||||
|
The following examples show various abstract examples that mirror the above rules however the AND-list is a list of
|
||||||
|
objects where the key is named `value`. This format should only be used for the configuration item type
|
||||||
|
`list(list(object))`, see [List of Lists](#list-of-lists) if you're not looking for a `list(list(object))`
|
||||||
|
|
||||||
|
##### Fully Expressed
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- - value: 'a'
|
||||||
|
- value: 'b'
|
||||||
|
- - value: 'c'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Omitted Level
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- - 'a'
|
||||||
|
- 'b'
|
||||||
|
- value: 'c'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Compact
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- ['a', 'b']
|
||||||
|
- ['c']
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Compact with Omitted Level
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule:
|
||||||
|
- ['a', 'b']
|
||||||
|
- 'c'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Super Compact
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rule: [['a', 'b'], ['c']]
|
||||||
|
```
|
|
@ -0,0 +1,119 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAccessControlQuery creates a new AccessControlQuery rule type.
|
||||||
|
func NewAccessControlQuery(config [][]schema.ACLQueryRule) (rules []AccessControlQuery) {
|
||||||
|
if len(config) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(config); i++ {
|
||||||
|
var rule []ObjectMatcher
|
||||||
|
|
||||||
|
for j := 0; j < len(config[i]); j++ {
|
||||||
|
subRule, err := NewAccessControlQueryObjectMatcher(config[i][j])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rule = append(rule, subRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, AccessControlQuery{Rules: rule})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessControlQuery represents an ACL query args rule.
|
||||||
|
type AccessControlQuery struct {
|
||||||
|
Rules []ObjectMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if this rule matches the object.
|
||||||
|
func (acq AccessControlQuery) IsMatch(object Object) (isMatch bool) {
|
||||||
|
for _, rule := range acq.Rules {
|
||||||
|
if !rule.IsMatch(object) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAccessControlQueryObjectMatcher creates a new ObjectMatcher rule type from a schema.ACLQueryRule.
|
||||||
|
func NewAccessControlQueryObjectMatcher(rule schema.ACLQueryRule) (matcher ObjectMatcher, err error) {
|
||||||
|
switch rule.Operator {
|
||||||
|
case operatorPresent, operatorAbsent:
|
||||||
|
return &AccessControlQueryMatcherPresent{key: rule.Key, present: rule.Operator == operatorPresent}, nil
|
||||||
|
case operatorEqual, operatorNotEqual:
|
||||||
|
if value, ok := rule.Value.(string); ok {
|
||||||
|
return &AccessControlQueryMatcherEqual{key: rule.Key, value: value, equal: rule.Operator == operatorEqual}, nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("rule value is not a string and is instead %T", rule.Value)
|
||||||
|
}
|
||||||
|
case operatorPattern, operatorNotPattern:
|
||||||
|
if pattern, ok := rule.Value.(*regexp.Regexp); ok {
|
||||||
|
return &AccessControlQueryMatcherPattern{key: rule.Key, pattern: pattern, match: rule.Operator == operatorPattern}, nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("rule value is not a *regexp.Regexp and is instead %T", rule.Value)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid operator: %s", rule.Operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessControlQueryMatcherEqual is a rule type that checks the equality of a query parameter.
|
||||||
|
type AccessControlQueryMatcherEqual struct {
|
||||||
|
key, value string
|
||||||
|
equal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if this rule matches the object.
|
||||||
|
func (acl AccessControlQueryMatcherEqual) IsMatch(object Object) (isMatch bool) {
|
||||||
|
switch {
|
||||||
|
case acl.equal:
|
||||||
|
return object.URL.Query().Get(acl.key) == acl.value
|
||||||
|
default:
|
||||||
|
return object.URL.Query().Get(acl.key) != acl.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessControlQueryMatcherPresent is a rule type that checks the presence of a query parameter.
|
||||||
|
type AccessControlQueryMatcherPresent struct {
|
||||||
|
key string
|
||||||
|
present bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if this rule matches the object.
|
||||||
|
func (acl AccessControlQueryMatcherPresent) IsMatch(object Object) (isMatch bool) {
|
||||||
|
switch {
|
||||||
|
case acl.present:
|
||||||
|
return object.URL.Query().Has(acl.key)
|
||||||
|
default:
|
||||||
|
return !object.URL.Query().Has(acl.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessControlQueryMatcherPattern is a rule type that checks a query parameter against regex.
|
||||||
|
type AccessControlQueryMatcherPattern struct {
|
||||||
|
key string
|
||||||
|
pattern *regexp.Regexp
|
||||||
|
match bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMatch returns true if this rule matches the object.
|
||||||
|
func (acl AccessControlQueryMatcherPattern) IsMatch(object Object) (isMatch bool) {
|
||||||
|
switch {
|
||||||
|
case acl.match:
|
||||||
|
return acl.pattern.MatchString(object.URL.Query().Get(acl.key))
|
||||||
|
default:
|
||||||
|
return !acl.pattern.MatchString(object.URL.Query().Get(acl.key))
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +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 {
|
||||||
r := &AccessControlRule{
|
r := &AccessControlRule{
|
||||||
Position: pos,
|
Position: pos,
|
||||||
|
Query: NewAccessControlQuery(rule.Query),
|
||||||
Methods: schemaMethodsToACL(rule.Methods),
|
Methods: schemaMethodsToACL(rule.Methods),
|
||||||
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
|
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
|
||||||
Subjects: schemaSubjectsToACL(rule.Subjects),
|
Subjects: schemaSubjectsToACL(rule.Subjects),
|
||||||
|
@ -46,6 +47,7 @@ type AccessControlRule struct {
|
||||||
Position int
|
Position int
|
||||||
Domains []AccessControlDomain
|
Domains []AccessControlDomain
|
||||||
Resources []AccessControlResource
|
Resources []AccessControlResource
|
||||||
|
Query []AccessControlQuery
|
||||||
Methods []string
|
Methods []string
|
||||||
Networks []*net.IPNet
|
Networks []*net.IPNet
|
||||||
Subjects []AccessControlSubjects
|
Subjects []AccessControlSubjects
|
||||||
|
@ -54,37 +56,42 @@ type AccessControlRule struct {
|
||||||
|
|
||||||
// IsMatch returns true if all elements of an AccessControlRule match the object and subject.
|
// IsMatch returns true if all elements of an AccessControlRule match the object and subject.
|
||||||
func (acr *AccessControlRule) IsMatch(subject Subject, object Object) (match bool) {
|
func (acr *AccessControlRule) IsMatch(subject Subject, object Object) (match bool) {
|
||||||
if !isMatchForDomains(subject, object, acr) {
|
if !acr.MatchesDomains(subject, object) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isMatchForResources(subject, object, acr) {
|
if !acr.MatchesResources(subject, object) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isMatchForMethods(object, acr) {
|
if !acr.MatchesQuery(object) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isMatchForNetworks(subject, acr) {
|
if !acr.MatchesMethods(object) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isMatchForSubjects(subject, acr) {
|
if !acr.MatchesNetworks(subject) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !acr.MatchesSubjects(subject) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMatchForDomains(subject Subject, object Object, acl *AccessControlRule) (match bool) {
|
// MatchesDomains returns true if the rule matches the domains.
|
||||||
|
func (acr *AccessControlRule) MatchesDomains(subject Subject, object Object) (matches bool) {
|
||||||
// If there are no domains in this rule then the domain condition is a match.
|
// If there are no domains in this rule then the domain condition is a match.
|
||||||
if len(acl.Domains) == 0 {
|
if len(acr.Domains) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over the domains until we find a match (return true) or until we exit the loop (return false).
|
// Iterate over the domains until we find a match (return true) or until we exit the loop (return false).
|
||||||
for _, domain := range acl.Domains {
|
for _, domain := range acr.Domains {
|
||||||
if domain.IsMatch(subject, object) {
|
if domain.IsMatch(subject, object) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -93,14 +100,15 @@ func isMatchForDomains(subject Subject, object Object, acl *AccessControlRule) (
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMatchForResources(subject Subject, object Object, acl *AccessControlRule) (match bool) {
|
// MatchesResources returns true if the rule matches the resources.
|
||||||
|
func (acr *AccessControlRule) MatchesResources(subject Subject, object Object) (matches bool) {
|
||||||
// If there are no resources in this rule then the resource condition is a match.
|
// If there are no resources in this rule then the resource condition is a match.
|
||||||
if len(acl.Resources) == 0 {
|
if len(acr.Resources) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over the resources until we find a match (return true) or until we exit the loop (return false).
|
// Iterate over the resources until we find a match (return true) or until we exit the loop (return false).
|
||||||
for _, resource := range acl.Resources {
|
for _, resource := range acr.Resources {
|
||||||
if resource.IsMatch(subject, object) {
|
if resource.IsMatch(subject, object) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -109,23 +117,42 @@ func isMatchForResources(subject Subject, object Object, acl *AccessControlRule)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMatchForMethods(object Object, acl *AccessControlRule) (match bool) {
|
// MatchesQuery returns true if the rule matches the query arguments.
|
||||||
// If there are no methods in this rule then the method condition is a match.
|
func (acr *AccessControlRule) MatchesQuery(object Object) (match bool) {
|
||||||
if len(acl.Methods) == 0 {
|
// If there are no query rules in this rule then the query condition is a match.
|
||||||
|
if len(acr.Query) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.IsStringInSlice(object.Method, acl.Methods)
|
// Iterate over the queries until we find a match (return true) or until we exit the loop (return false).
|
||||||
|
for _, query := range acr.Query {
|
||||||
|
if query.IsMatch(object) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMatchForNetworks(subject Subject, acl *AccessControlRule) (match bool) {
|
// MatchesMethods returns true if the rule matches the method.
|
||||||
|
func (acr *AccessControlRule) MatchesMethods(object Object) (match bool) {
|
||||||
|
// If there are no methods in this rule then the method condition is a match.
|
||||||
|
if len(acr.Methods) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.IsStringInSlice(object.Method, acr.Methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesNetworks returns true if the rule matches the networks.
|
||||||
|
func (acr *AccessControlRule) MatchesNetworks(subject Subject) (match bool) {
|
||||||
// If there are no networks in this rule then the network condition is a match.
|
// If there are no networks in this rule then the network condition is a match.
|
||||||
if len(acl.Networks) == 0 {
|
if len(acr.Networks) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over the networks until we find a match (return true) or until we exit the loop (return false).
|
// Iterate over the networks until we find a match (return true) or until we exit the loop (return false).
|
||||||
for _, network := range acl.Networks {
|
for _, network := range acr.Networks {
|
||||||
if network.Contains(subject.IP) {
|
if network.Contains(subject.IP) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -134,25 +161,26 @@ func isMatchForNetworks(subject Subject, acl *AccessControlRule) (match bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as isExactMatchForSubjects except it theoretically matches if subject is anonymous since they'd need to authenticate.
|
// MatchesSubjects returns true if the rule matches the subjects.
|
||||||
func isMatchForSubjects(subject Subject, acl *AccessControlRule) (match bool) {
|
func (acr *AccessControlRule) MatchesSubjects(subject Subject) (match bool) {
|
||||||
if subject.IsAnonymous() {
|
if subject.IsAnonymous() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return isExactMatchForSubjects(subject, acl)
|
return acr.MatchesSubjectExact(subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isExactMatchForSubjects(subject Subject, acl *AccessControlRule) (match bool) {
|
// MatchesSubjectExact returns true if the rule matches the subjects exactly.
|
||||||
|
func (acr *AccessControlRule) MatchesSubjectExact(subject Subject) (match bool) {
|
||||||
// If there are no subjects in this rule then the subject condition is a match.
|
// If there are no subjects in this rule then the subject condition is a match.
|
||||||
if len(acl.Subjects) == 0 {
|
if len(acr.Subjects) == 0 {
|
||||||
return true
|
return true
|
||||||
} else if subject.IsAnonymous() {
|
} else if subject.IsAnonymous() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over the subjects until we find a match (return true) or until we exit the loop (return false).
|
// Iterate over the subjects until we find a match (return true) or until we exit the loop (return false).
|
||||||
for _, subjectRule := range acl.Subjects {
|
for _, subjectRule := range acr.Subjects {
|
||||||
if subjectRule.IsMatch(subject) {
|
if subjectRule.IsMatch(subject) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,12 +88,13 @@ func (p Authorizer) GetRuleMatchResults(subject Subject, object Object) (results
|
||||||
Rule: rule,
|
Rule: rule,
|
||||||
Skipped: skipped,
|
Skipped: skipped,
|
||||||
|
|
||||||
MatchDomain: isMatchForDomains(subject, object, rule),
|
MatchDomain: rule.MatchesDomains(subject, object),
|
||||||
MatchResources: isMatchForResources(subject, object, rule),
|
MatchResources: rule.MatchesResources(subject, object),
|
||||||
MatchMethods: isMatchForMethods(object, rule),
|
MatchQuery: rule.MatchesQuery(object),
|
||||||
MatchNetworks: isMatchForNetworks(subject, rule),
|
MatchMethods: rule.MatchesMethods(object),
|
||||||
MatchSubjects: isMatchForSubjects(subject, rule),
|
MatchNetworks: rule.MatchesNetworks(subject),
|
||||||
MatchSubjectsExact: isExactMatchForSubjects(subject, rule),
|
MatchSubjects: rule.MatchesSubjects(subject),
|
||||||
|
MatchSubjectsExact: rule.MatchesSubjectExact(subject),
|
||||||
}
|
}
|
||||||
|
|
||||||
skipped = skipped || results[i].IsMatch()
|
skipped = skipped || results[i].IsMatch()
|
||||||
|
|
|
@ -208,6 +208,129 @@ func (s *AuthorizerSuite) TestShouldCheckFactorsPolicy() {
|
||||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://example.com/", "GET", Denied)
|
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://example.com/", "GET", Denied)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AuthorizerSuite) TestShouldCheckQueryPolicy() {
|
||||||
|
tester := NewAuthorizerBuilder().
|
||||||
|
WithDefaultPolicy(deny).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"one.example.com"},
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorEqual,
|
||||||
|
Key: "test",
|
||||||
|
Value: "two",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Operator: operatorAbsent,
|
||||||
|
Key: "admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorPresent,
|
||||||
|
Key: "public",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policy: oneFactor,
|
||||||
|
}).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"two.example.com"},
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorEqual,
|
||||||
|
Key: "test",
|
||||||
|
Value: "one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorEqual,
|
||||||
|
Key: "test",
|
||||||
|
Value: "two",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policy: twoFactor,
|
||||||
|
}).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"three.example.com"},
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorNotEqual,
|
||||||
|
Key: "test",
|
||||||
|
Value: "one",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Operator: operatorNotEqual,
|
||||||
|
Key: "test",
|
||||||
|
Value: "two",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policy: twoFactor,
|
||||||
|
}).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"four.example.com"},
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorPattern,
|
||||||
|
Key: "test",
|
||||||
|
Value: regexp.MustCompile(`^(one|two|three)$`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policy: twoFactor,
|
||||||
|
}).
|
||||||
|
WithRule(schema.ACLRule{
|
||||||
|
Domains: []string{"five.example.com"},
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Operator: operatorNotPattern,
|
||||||
|
Key: "test",
|
||||||
|
Value: regexp.MustCompile(`^(one|two|three)$`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policy: twoFactor,
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name, requestURL string
|
||||||
|
expected Level
|
||||||
|
}{
|
||||||
|
{"ShouldDenyAbsentRule", "https://one.example.com/?admin=true", Denied},
|
||||||
|
{"ShouldAllow1FAPresentRule", "https://one.example.com/?public=true", OneFactor},
|
||||||
|
{"ShouldAllow1FAEqualRule", "https://one.example.com/?test=two", OneFactor},
|
||||||
|
{"ShouldDenyAbsentRuleWithMatchingPresentRule", "https://one.example.com/?test=two&admin=true", Denied},
|
||||||
|
{"ShouldAllow2FARuleWithOneMatchingEqual", "https://two.example.com/?test=one&admin=true", TwoFactor},
|
||||||
|
{"ShouldAllow2FARuleWithAnotherMatchingEqual", "https://two.example.com/?test=two&admin=true", TwoFactor},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingEqual", "https://two.example.com/?test=three&admin=true", Denied},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingNotEqualAND1", "https://three.example.com/?test=one", Denied},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingNotEqualAND2", "https://three.example.com/?test=two", Denied},
|
||||||
|
{"ShouldAllowRuleWithMatchingNotEqualAND", "https://three.example.com/?test=three", TwoFactor},
|
||||||
|
{"ShouldAllowRuleWithMatchingPatternOne", "https://four.example.com/?test=one", TwoFactor},
|
||||||
|
{"ShouldAllowRuleWithMatchingPatternTwo", "https://four.example.com/?test=two", TwoFactor},
|
||||||
|
{"ShouldAllowRuleWithMatchingPatternThree", "https://four.example.com/?test=three", TwoFactor},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingPattern", "https://four.example.com/?test=five", Denied},
|
||||||
|
{"ShouldAllowRuleWithMatchingNotPattern", "https://five.example.com/?test=five", TwoFactor},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingNotPatternOne", "https://five.example.com/?test=one", Denied},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingNotPatternTwo", "https://five.example.com/?test=two", Denied},
|
||||||
|
{"ShouldDenyRuleWithNotMatchingNotPatternThree", "https://five.example.com/?test=three", Denied},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
tester.CheckAuthorizations(t, UserWithGroups, tc.requestURL, "GET", tc.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
|
func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
|
||||||
tester := NewAuthorizerBuilder().
|
tester := NewAuthorizerBuilder().
|
||||||
WithDefaultPolicy(deny).
|
WithDefaultPolicy(deny).
|
||||||
|
|
|
@ -26,6 +26,15 @@ const (
|
||||||
deny = "deny"
|
deny = "deny"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
operatorPresent = "present"
|
||||||
|
operatorAbsent = "absent"
|
||||||
|
operatorEqual = "equal"
|
||||||
|
operatorNotEqual = "not equal"
|
||||||
|
operatorPattern = "pattern"
|
||||||
|
operatorNotPattern = "not pattern"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
subexpNameUser = "User"
|
subexpNameUser = "User"
|
||||||
subexpNameGroup = "Group"
|
subexpNameGroup = "Group"
|
||||||
|
|
|
@ -24,6 +24,11 @@ type SubjectObjectMatcher interface {
|
||||||
IsMatch(subject Subject, object Object) (match bool)
|
IsMatch(subject Subject, object Object) (match bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ObjectMatcher is a matcher that takes an object.
|
||||||
|
type ObjectMatcher interface {
|
||||||
|
IsMatch(object Object) (match bool)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -43,7 +48,7 @@ func (s Subject) IsAnonymous() bool {
|
||||||
|
|
||||||
// Object represents a protected object for the purposes of ACL matching.
|
// Object represents a protected object for the purposes of ACL matching.
|
||||||
type Object struct {
|
type Object struct {
|
||||||
URL url.URL
|
URL *url.URL
|
||||||
|
|
||||||
Domain string
|
Domain string
|
||||||
Path string
|
Path string
|
||||||
|
@ -63,7 +68,7 @@ func NewObjectRaw(targetURL *url.URL, method []byte) (object Object) {
|
||||||
// NewObject creates a new Object type from a URL and a method header.
|
// NewObject creates a new Object type from a URL and a method header.
|
||||||
func NewObject(targetURL *url.URL, method string) (object Object) {
|
func NewObject(targetURL *url.URL, method string) (object Object) {
|
||||||
return Object{
|
return Object{
|
||||||
URL: *targetURL,
|
URL: targetURL,
|
||||||
Domain: targetURL.Hostname(),
|
Domain: targetURL.Hostname(),
|
||||||
Path: utils.URLPathFullClean(targetURL),
|
Path: utils.URLPathFullClean(targetURL),
|
||||||
Method: method,
|
Method: method,
|
||||||
|
@ -78,6 +83,7 @@ type RuleMatchResult struct {
|
||||||
|
|
||||||
MatchDomain bool
|
MatchDomain bool
|
||||||
MatchResources bool
|
MatchResources bool
|
||||||
|
MatchQuery bool
|
||||||
MatchMethods bool
|
MatchMethods bool
|
||||||
MatchNetworks bool
|
MatchNetworks bool
|
||||||
MatchSubjects bool
|
MatchSubjects bool
|
||||||
|
|
|
@ -480,7 +480,7 @@ func cmdCryptoHashGetPassword(cmd *cobra.Command, args []string, useArgs, useRan
|
||||||
func hashReadPasswordWithPrompt(prompt string) (data []byte, err error) {
|
func hashReadPasswordWithPrompt(prompt string) (data []byte, err error) {
|
||||||
fmt.Print(prompt)
|
fmt.Print(prompt)
|
||||||
|
|
||||||
if data, err = term.ReadPassword(int(syscall.Stdin)); err != nil { //nolint:unconvert // Conversion required.
|
if data, err = term.ReadPassword(int(syscall.Stdin)); err != nil { //nolint:unconvert,nolintlint
|
||||||
if err.Error() == "inappropriate ioctl for device" {
|
if err.Error() == "inappropriate ioctl for device" {
|
||||||
return nil, fmt.Errorf("the terminal doesn't appear to be interactive either use the '--password' flag or use an interactive terminal: %w", err)
|
return nil, fmt.Errorf("the terminal doesn't appear to be interactive either use the '--password' flag or use an interactive terminal: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,14 @@ type ACLRule struct {
|
||||||
Networks []string `koanf:"networks"`
|
Networks []string `koanf:"networks"`
|
||||||
Resources []regexp.Regexp `koanf:"resources"`
|
Resources []regexp.Regexp `koanf:"resources"`
|
||||||
Methods []string `koanf:"methods"`
|
Methods []string `koanf:"methods"`
|
||||||
|
Query [][]ACLQueryRule `koanf:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACLQueryRule represents the ACL query criteria.
|
||||||
|
type ACLQueryRule struct {
|
||||||
|
Operator string `koanf:"operator"`
|
||||||
|
Key string `koanf:"key"`
|
||||||
|
Value any `koanf:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultACLNetwork represents the default configuration related to access control network group configuration.
|
// DefaultACLNetwork represents the default configuration related to access control network group configuration.
|
||||||
|
|
|
@ -148,6 +148,10 @@ var Keys = []string{
|
||||||
"access_control.rules[].networks",
|
"access_control.rules[].networks",
|
||||||
"access_control.rules[].resources",
|
"access_control.rules[].resources",
|
||||||
"access_control.rules[].methods",
|
"access_control.rules[].methods",
|
||||||
|
"access_control.rules[].query[][].operator",
|
||||||
|
"access_control.rules[].query[][].key",
|
||||||
|
"access_control.rules[].query[][].value",
|
||||||
|
"access_control.rules[].query",
|
||||||
"ntp.address",
|
"ntp.address",
|
||||||
"ntp.version",
|
"ntp.version",
|
||||||
"ntp.max_desync",
|
"ntp.max_desync",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package validator
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
|
@ -103,6 +104,8 @@ func ValidateRules(config *schema.Configuration, validator *schema.StructValidat
|
||||||
|
|
||||||
validateMethods(rulePosition, rule, validator)
|
validateMethods(rulePosition, rule, validator)
|
||||||
|
|
||||||
|
validateQuery(i, rule, config, validator)
|
||||||
|
|
||||||
if rule.Policy == policyBypass {
|
if rule.Policy == policyBypass {
|
||||||
validateBypass(rulePosition, rule, validator)
|
validateBypass(rulePosition, rule, validator)
|
||||||
}
|
}
|
||||||
|
@ -149,3 +152,60 @@ func validateMethods(rulePosition int, rule schema.ACLRule, validator *schema.St
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func validateQuery(i int, rule schema.ACLRule, config *schema.Configuration, validator *schema.StructValidator) {
|
||||||
|
for j := 0; j < len(config.AccessControl.Rules[i].Query); j++ {
|
||||||
|
for k := 0; k < len(config.AccessControl.Rules[i].Query[j]); k++ {
|
||||||
|
if config.AccessControl.Rules[i].Query[j][k].Operator == "" {
|
||||||
|
if config.AccessControl.Rules[i].Query[j][k].Key != "" {
|
||||||
|
switch config.AccessControl.Rules[i].Query[j][k].Value {
|
||||||
|
case "", nil:
|
||||||
|
config.AccessControl.Rules[i].Query[j][k].Operator = operatorPresent
|
||||||
|
default:
|
||||||
|
config.AccessControl.Rules[i].Query[j][k].Operator = operatorEqual
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !utils.IsStringInSliceFold(config.AccessControl.Rules[i].Query[j][k].Operator, validACLRuleOperators) {
|
||||||
|
validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalid, ruleDescriptor(i+1, rule), config.AccessControl.Rules[i].Query[j][k].Operator, strings.Join(validACLRuleOperators, "', '")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AccessControl.Rules[i].Query[j][k].Key == "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidNoValue, ruleDescriptor(i+1, rule), "key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
op := config.AccessControl.Rules[i].Query[j][k].Operator
|
||||||
|
|
||||||
|
if op == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := config.AccessControl.Rules[i].Query[j][k].Value.(type) {
|
||||||
|
case nil:
|
||||||
|
if op != operatorAbsent && op != operatorPresent {
|
||||||
|
validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidNoValueOperator, ruleDescriptor(i+1, rule), "value", op))
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
switch op {
|
||||||
|
case operatorPresent, operatorAbsent:
|
||||||
|
if v != "" {
|
||||||
|
validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidValue, ruleDescriptor(i+1, rule), "value", op))
|
||||||
|
}
|
||||||
|
case operatorPattern, operatorNotPattern:
|
||||||
|
var (
|
||||||
|
pattern *regexp.Regexp
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if pattern, err = regexp.Compile(v); err != nil {
|
||||||
|
validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidValueParse, ruleDescriptor(i+1, rule), "value", err))
|
||||||
|
} else {
|
||||||
|
config.AccessControl.Rules[i].Query[j][k].Value = pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
validator.Push(fmt.Errorf(errFmtAccessControlRuleQueryInvalidValueType, ruleDescriptor(i+1, rule), v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -201,6 +201,149 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
|
||||||
suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0])))
|
suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0])))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *AccessControl) TestShouldSetQueryDefaults() {
|
||||||
|
domains := []string{"public.example.com"}
|
||||||
|
suite.config.AccessControl.Rules = []schema.ACLRule{
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "", Key: "example"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{Operator: "", Key: "example", Value: "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "pattern", Key: "a", Value: "^(x|y|z)$"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateRules(suite.config, suite.validator)
|
||||||
|
|
||||||
|
suite.Assert().Len(suite.validator.Warnings(), 0)
|
||||||
|
suite.Assert().Len(suite.validator.Errors(), 0)
|
||||||
|
|
||||||
|
suite.Assert().Equal("present", suite.config.AccessControl.Rules[0].Query[0][0].Operator)
|
||||||
|
suite.Assert().Equal("equal", suite.config.AccessControl.Rules[0].Query[1][0].Operator)
|
||||||
|
|
||||||
|
suite.Require().Len(suite.config.AccessControl.Rules, 2)
|
||||||
|
suite.Require().Len(suite.config.AccessControl.Rules[1].Query, 1)
|
||||||
|
suite.Require().Len(suite.config.AccessControl.Rules[1].Query[0], 1)
|
||||||
|
|
||||||
|
t := ®exp.Regexp{}
|
||||||
|
|
||||||
|
suite.Assert().IsType(t, suite.config.AccessControl.Rules[1].Query[0][0].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AccessControl) TestShouldErrorOnInvalidRulesQuery() {
|
||||||
|
domains := []string{"public.example.com"}
|
||||||
|
suite.config.AccessControl.Rules = []schema.ACLRule{
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "equal", Key: "example"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "present"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "present", Key: "a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "absent", Key: "a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "not", Key: "a", Value: "a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "pattern", Key: "a", Value: "(bad pattern"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "present", Key: "a", Value: "not good"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Domains: domains,
|
||||||
|
Policy: "bypass",
|
||||||
|
Query: [][]schema.ACLQueryRule{
|
||||||
|
{
|
||||||
|
{Operator: "present", Key: "a", Value: 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateRules(suite.config, suite.validator)
|
||||||
|
|
||||||
|
suite.Assert().Len(suite.validator.Warnings(), 0)
|
||||||
|
suite.Require().Len(suite.validator.Errors(), 7)
|
||||||
|
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'query' option 'value' is invalid: must have a value when the operator is 'equal'")
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[1], "access control: rule #2 (domain 'public.example.com'): 'query' option 'key' is invalid: must have a value")
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[2], "access control: rule #5 (domain 'public.example.com'): 'query' option 'key' is invalid: must have a value")
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[3], "access control: rule #6 (domain 'public.example.com'): 'query' option 'operator' with value 'not' is invalid: must be one of 'present', 'absent', 'equal', 'not equal', 'pattern', 'not pattern'")
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[4], "access control: rule #7 (domain 'public.example.com'): 'query' option 'value' is invalid: error parsing regexp: missing closing ): `(bad pattern`")
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[5], "access control: rule #8 (domain 'public.example.com'): 'query' option 'value' is invalid: must not have a value when the operator is 'present'")
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[6], "access control: rule #9 (domain 'public.example.com'): 'query' option 'value' is invalid: expected type was string but got int")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccessControl(t *testing.T) {
|
func TestAccessControl(t *testing.T) {
|
||||||
suite.Run(t, new(AccessControl))
|
suite.Run(t, new(AccessControl))
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,6 +211,18 @@ const (
|
||||||
"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 " +
|
||||||
"invalid: must be one of '%s'"
|
"invalid: must be one of '%s'"
|
||||||
|
errFmtAccessControlRuleQueryInvalid = "access control: rule %s: 'query' option 'operator' with value '%s' is " +
|
||||||
|
"invalid: must be one of '%s'"
|
||||||
|
errFmtAccessControlRuleQueryInvalidNoValue = "access control: rule %s: 'query' option '%s' is " +
|
||||||
|
"invalid: must have a value"
|
||||||
|
errFmtAccessControlRuleQueryInvalidNoValueOperator = "access control: rule %s: 'query' option '%s' is " +
|
||||||
|
"invalid: must have a value when the operator is '%s'"
|
||||||
|
errFmtAccessControlRuleQueryInvalidValue = "access control: rule %s: 'query' option '%s' is " +
|
||||||
|
"invalid: must not have a value when the operator is '%s'"
|
||||||
|
errFmtAccessControlRuleQueryInvalidValueParse = "access control: rule %s: 'query' option '%s' is " +
|
||||||
|
"invalid: %w"
|
||||||
|
errFmtAccessControlRuleQueryInvalidValueType = "access control: rule %s: 'query' option 'value' is " +
|
||||||
|
"invalid: expected type was string but got %T"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Theme Error constants.
|
// Theme Error constants.
|
||||||
|
@ -317,9 +329,20 @@ var validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH"
|
||||||
|
|
||||||
var validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"}
|
var validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"}
|
||||||
|
|
||||||
var validACLHTTPMethodVerbs = append(validRFC7231HTTPMethodVerbs, validRFC4918HTTPMethodVerbs...)
|
const (
|
||||||
|
operatorPresent = "present"
|
||||||
|
operatorAbsent = "absent"
|
||||||
|
operatorEqual = "equal"
|
||||||
|
operatorNotEqual = "not equal"
|
||||||
|
operatorPattern = "pattern"
|
||||||
|
operatorNotPattern = "not pattern"
|
||||||
|
)
|
||||||
|
|
||||||
var validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny}
|
var (
|
||||||
|
validACLHTTPMethodVerbs = append(validRFC7231HTTPMethodVerbs, validRFC4918HTTPMethodVerbs...)
|
||||||
|
validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny}
|
||||||
|
validACLRuleOperators = []string{operatorPresent, operatorAbsent, operatorEqual, operatorNotEqual, operatorPattern, operatorNotPattern}
|
||||||
|
)
|
||||||
|
|
||||||
var validDefault2FAMethods = []string{"totp", "webauthn", "mobile_push"}
|
var validDefault2FAMethods = []string{"totp", "webauthn", "mobile_push"}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue