[MISC] Validate all sections of ACLs on startup (#1595)

* [MISC] Validate all sections of ACLs on startup

This change ensure that all sections of the `access_control` key are validated on startup.

* Change error format to clearly identify values
pull/1597/head^2
Amir Zarrinkafsh 2021-01-16 21:05:41 +11:00 committed by GitHub
parent 57c339bb96
commit 81e34d84de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 65 deletions

View File

@ -1,7 +1,5 @@
package authorization package authorization
import "github.com/authelia/authelia/internal/configuration/schema"
// Level is the type representing an authorization level. // Level is the type representing an authorization level.
type Level int type Level int
@ -15,14 +13,3 @@ const (
// Denied denied level. // Denied denied level.
Denied Level = iota Denied Level = iota
) )
var testACLNetwork = []schema.ACLNetwork{
{
Name: []string{"localhost"},
Networks: []string{"127.0.0.1"},
},
{
Name: []string{"internal"},
Networks: []string{"10.0.0.0/8"},
},
}

View File

@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging"
) )
func selectMatchingNetworkGroups(networks []string, aclNetworks []schema.ACLNetwork) []schema.ACLNetwork { func selectMatchingNetworkGroups(networks []string, aclNetworks []schema.ACLNetwork) []schema.ACLNetwork {
@ -36,16 +35,8 @@ func isIPAddressOrCIDR(ip net.IP, network string) bool {
} }
func parseCIDR(ip net.IP, network string) bool { func parseCIDR(ip net.IP, network string) bool {
_, ipNet, err := net.ParseCIDR(network) _, ipNet, _ := net.ParseCIDR(network)
if err != nil { return ipNet.Contains(ip)
logging.Logger().Errorf("Failed to parse network %s: %s", network, err)
}
if ipNet.Contains(ip) {
return true
}
return false
} }
// isIPMatching check whether user's IP is in one of the network ranges. // isIPMatching check whether user's IP is in one of the network ranges.

View File

@ -5,32 +5,34 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/authelia/authelia/internal/configuration/schema"
) )
func TestIPMatcher(t *testing.T) { func TestIPMatcher(t *testing.T) {
// Default policy is 'allow all ips' if no IP is defined // Default policy is 'allow all ips' if no IP is defined
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{}, schema.DefaultACLNetwork))
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"127.0.0.1"}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"127.0.0.1"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"127.0.0.1"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"127.0.0.1"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"127.0.0.1"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"127.0.0.1"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"10.0.0.1"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"10.0.0.1"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"10.0.0.0/8"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"10.0.0.0/8"}, schema.DefaultACLNetwork))
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"10.0.0.0/8"}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"10.0.0.0/8"}, schema.DefaultACLNetwork))
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"192.168.0.0/24", "10.0.0.0/8"}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"192.168.0.0/24", "10.0.0.0/8"}, schema.DefaultACLNetwork))
// Test network groups // Test network groups
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{}, schema.DefaultACLNetwork))
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"localhost"}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"localhost"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"localhost"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"localhost"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"localhost"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"localhost"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"internal"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"internal"}, schema.DefaultACLNetwork))
assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"internal"}, testACLNetwork)) assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"internal"}, schema.DefaultACLNetwork))
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"internal"}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"internal"}, schema.DefaultACLNetwork))
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"192.168.0.0/24", "internal"}, testACLNetwork)) assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"192.168.0.0/24", "internal"}, schema.DefaultACLNetwork))
} }

View File

@ -9,13 +9,7 @@ func isPathMatching(path string, pathRegexps []string) bool {
} }
for _, pathRegexp := range pathRegexps { for _, pathRegexp := range pathRegexps {
match, err := regexp.MatchString(pathRegexp, path) match, _ := regexp.MatchString(pathRegexp, path)
if err != nil {
// TODO(c.michaud): make sure this is safe in advance to
// avoid checking this case here.
continue
}
if match { if match {
return true return true
} }

View File

@ -8,11 +8,6 @@ import (
func isSubjectMatching(subject Subject, subjectRule []string) bool { func isSubjectMatching(subject Subject, subjectRule []string) bool {
for _, ruleSubject := range subjectRule { for _, ruleSubject := range subjectRule {
// If no subject is provided in the rule, we match any user.
if ruleSubject == "" {
continue
}
if strings.HasPrefix(ruleSubject, userPrefix) { if strings.HasPrefix(ruleSubject, userPrefix) {
user := strings.Trim(ruleSubject[len(userPrefix):], " ") user := strings.Trim(ruleSubject[len(userPrefix):], " ")
if user == subject.Username { if user == subject.Username {

View File

@ -21,3 +21,31 @@ type ACLRule struct {
Networks []string `mapstructure:"networks"` Networks []string `mapstructure:"networks"`
Resources []string `mapstructure:"resources"` Resources []string `mapstructure:"resources"`
} }
// DefaultACLNetwork represents the default configuration related to access control network group configuration.
var DefaultACLNetwork = []ACLNetwork{
{
Name: []string{"localhost"},
Networks: []string{"127.0.0.1"},
},
{
Name: []string{"internal"},
Networks: []string{"10.0.0.0/8"},
},
}
// DefaultACLRule represents the default configuration related to access control rule configuration.
var DefaultACLRule = []ACLRule{
{
Domains: []string{"public.example.com"},
Policy: "bypass",
},
{
Domains: []string{"singlefactor.example.com"},
Policy: "one_factor",
},
{
Domains: []string{"secure.example.com"},
Policy: "two_factor",
},
}

View File

@ -3,6 +3,7 @@ package validator
import ( import (
"fmt" "fmt"
"net" "net"
"regexp"
"strings" "strings"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
@ -14,6 +15,12 @@ func IsPolicyValid(policy string) bool {
return policy == denyPolicy || policy == "one_factor" || policy == "two_factor" || policy == "bypass" return policy == denyPolicy || policy == "one_factor" || policy == "two_factor" || policy == "bypass"
} }
// IsResourceValid check if a resource is valid.
func IsResourceValid(resource string) 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) bool { func IsSubjectValid(subject string) bool {
return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:") return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
@ -52,7 +59,7 @@ func ValidateAccessControl(configuration schema.AccessControlConfiguration, vali
for _, n := range configuration.Networks { for _, n := range configuration.Networks {
for _, networks := range n.Networks { for _, networks := range n.Networks {
if !IsNetworkValid(networks) { if !IsNetworkValid(networks) {
validator.Push(fmt.Errorf("Network %s from group %s must be a valid IP or CIDR", networks, n.Name)) validator.Push(fmt.Errorf("Network %s from network group: %s must be a valid IP or CIDR", n.Networks, n.Name))
} }
} }
} }
@ -67,21 +74,27 @@ func ValidateRules(configuration schema.AccessControlConfiguration, validator *s
} }
if !IsPolicyValid(r.Policy) { if !IsPolicyValid(r.Policy) {
validator.Push(fmt.Errorf("Policy for domain: %s is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'", r.Domains)) validator.Push(fmt.Errorf("Policy [%s] for domain: %s is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'", r.Policy, r.Domains))
}
for i, subjectRule := range r.Subjects {
for j, subject := range subjectRule {
if !IsSubjectValid(subject) {
validator.Push(fmt.Errorf("Subject %d-%d must start with 'user:' or 'group:'", i, j))
}
}
} }
for _, network := range r.Networks { for _, network := range r.Networks {
if !IsNetworkValid(network) { if !IsNetworkValid(network) {
if !IsNetworkGroupValid(configuration, network) { if !IsNetworkGroupValid(configuration, network) {
validator.Push(fmt.Errorf("Network %s is not a valid network or network group", network)) validator.Push(fmt.Errorf("Network %s for domain: %s is not a valid network or network group", r.Networks, r.Domains))
}
}
}
for _, resource := range r.Resources {
if err := IsResourceValid(resource); err != nil {
validator.Push(fmt.Errorf("Resource %s for domain: %s is invalid, %s", r.Resources, r.Domains, err))
}
}
for _, subjectRule := range r.Subjects {
for _, subject := range subjectRule {
if !IsSubjectValid(subject) {
validator.Push(fmt.Errorf("Subject %s for domain: %s must start with 'user:' or 'group:'", subjectRule, r.Domains))
} }
} }
} }

View File

@ -0,0 +1,139 @@
package validator
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/internal/configuration/schema"
)
type AccessControl struct {
suite.Suite
configuration schema.AccessControlConfiguration
validator *schema.StructValidator
}
func (suite *AccessControl) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.configuration.DefaultPolicy = denyPolicy
suite.configuration.Networks = schema.DefaultACLNetwork
suite.configuration.Rules = schema.DefaultACLRule
}
func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
ValidateAccessControl(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors())
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
suite.configuration.DefaultPolicy = "invalid"
ValidateAccessControl(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'")
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidNetworkGroupNetwork() {
suite.configuration.Networks = []schema.ACLNetwork{
{
Name: []string{"internal"},
Networks: []string{"abc.def.ghi.jkl"},
},
}
ValidateAccessControl(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Network [abc.def.ghi.jkl] from network group: [internal] must be a valid IP or CIDR")
}
func (suite *AccessControl) TestShouldRaiseErrorNoRulesDefined() {
suite.configuration.Rules = []schema.ACLRule{{}}
ValidateRules(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[0], "No access control rules have been defined")
suite.Assert().EqualError(suite.validator.Errors()[1], "Policy [] for domain: [] is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'")
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() {
suite.configuration.Rules = []schema.ACLRule{
{
Domains: []string{"public.example.com"},
Policy: "invalid",
},
}
ValidateRules(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Policy [invalid] for domain: [public.example.com] is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'")
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidNetwork() {
suite.configuration.Rules = []schema.ACLRule{
{
Domains: []string{"public.example.com"},
Policy: "bypass",
Networks: []string{"abc.def.ghi.jkl/32"},
},
}
ValidateRules(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Network [abc.def.ghi.jkl/32] for domain: [public.example.com] is not a valid network or network group")
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidResource() {
suite.configuration.Rules = []schema.ACLRule{
{
Domains: []string{"public.example.com"},
Policy: "bypass",
Resources: []string{"^/(api.*"},
},
}
ValidateRules(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Resource [^/(api.*] for domain: [public.example.com] is invalid, error parsing regexp: missing closing ): `^/(api.*`")
}
func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
suite.configuration.Rules = []schema.ACLRule{
{
Domains: []string{"public.example.com"},
Policy: "bypass",
Subjects: [][]string{{"invalid"}},
},
}
ValidateRules(suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Subject [invalid] for domain: [public.example.com] must start with 'user:' or 'group:'")
}
func TestAccessControl(t *testing.T) {
suite.Run(t, new(AccessControl))
}

View File

@ -61,7 +61,7 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator) ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator)
if configuration.AccessControl.DefaultPolicy == "" { if configuration.AccessControl.DefaultPolicy == "" {
configuration.AccessControl.DefaultPolicy = "deny" configuration.AccessControl.DefaultPolicy = denyPolicy
} }
ValidateAccessControl(configuration.AccessControl, validator) ValidateAccessControl(configuration.AccessControl, validator)

View File

@ -47,7 +47,7 @@ duo_api:
# resources. # resources.
access_control: access_control:
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
default_policy: deny default_policy: two_factor
rules: rules:
- domain: singlefactor.example.com - domain: singlefactor.example.com