[FEATURE] Validate ACLs and add network groups (#1568)
* adds validation to ACL's * adds a new networks section that can be used as aliases in other sections (currently access_control)pull/1578/head
parent
29a900226d
commit
9ca0e940da
|
@ -244,6 +244,14 @@ access_control:
|
||||||
# to the user.
|
# to the user.
|
||||||
default_policy: deny
|
default_policy: deny
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- name: internal
|
||||||
|
networks:
|
||||||
|
- 10.10.0.0/16
|
||||||
|
- 192.168.2.0/24
|
||||||
|
- name: VPN
|
||||||
|
networks: 10.9.0.0/16
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
# Rules applied to everyone
|
# Rules applied to everyone
|
||||||
- domain: public.example.com
|
- domain: public.example.com
|
||||||
|
@ -253,7 +261,10 @@ access_control:
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
# Network based rule, if not provided any network matches.
|
# Network based rule, if not provided any network matches.
|
||||||
networks:
|
networks:
|
||||||
|
- internal
|
||||||
|
- VPN
|
||||||
- 192.168.1.0/24
|
- 192.168.1.0/24
|
||||||
|
- 10.0.0.1
|
||||||
|
|
||||||
- domain:
|
- domain:
|
||||||
- secure.example.com
|
- secure.example.com
|
||||||
|
|
|
@ -29,7 +29,7 @@ The criteria are:
|
||||||
* domain: domain targeted by the request.
|
* domain: domain targeted by the request.
|
||||||
* resources: list of patterns that the path should match (one is sufficient).
|
* resources: list of patterns that the path should match (one is sufficient).
|
||||||
* subject: the user or group of users to define the policy for.
|
* subject: the user or group of users to define the policy for.
|
||||||
* networks: the network range from where should comes the request.
|
* networks: the network addresses, ranges (CIDR notation) or groups from where the request originates.
|
||||||
|
|
||||||
A rule is matched when all criteria of the rule match.
|
A rule is matched when all criteria of the rule match.
|
||||||
|
|
||||||
|
@ -88,8 +88,8 @@ username is `john` OR the group is `admins`.
|
||||||
|
|
||||||
## Networks
|
## Networks
|
||||||
|
|
||||||
A list of network ranges can be specified in a rule in order to apply different policies when
|
A list of network addresses, ranges (CIDR notation) or groups can be specified in a rule in order to apply different
|
||||||
requests come from different networks.
|
policies when requests originate from different networks.
|
||||||
|
|
||||||
The main use case is when, lets say a resource should be exposed both on the Internet and from an
|
The main use case is when, lets say a resource should be exposed both on the Internet and from an
|
||||||
authenticated VPN for instance. Passing a second factor a first time to get access to the VPN and
|
authenticated VPN for instance. Passing a second factor a first time to get access to the VPN and
|
||||||
|
@ -108,6 +108,13 @@ Here is a complete example of complex access control list that can be defined in
|
||||||
```yaml
|
```yaml
|
||||||
access_control:
|
access_control:
|
||||||
default_policy: deny
|
default_policy: deny
|
||||||
|
networks:
|
||||||
|
- name: internal
|
||||||
|
networks:
|
||||||
|
- 10.10.0.0/16
|
||||||
|
- 192.168.2.0/24
|
||||||
|
- name: VPN
|
||||||
|
networks: 10.9.0.0/16
|
||||||
rules:
|
rules:
|
||||||
- domain: public.example.com
|
- domain: public.example.com
|
||||||
policy: bypass
|
policy: bypass
|
||||||
|
@ -115,7 +122,10 @@ access_control:
|
||||||
- domain: secure.example.com
|
- domain: secure.example.com
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
networks:
|
networks:
|
||||||
- 192.168.1.0/24
|
- internal
|
||||||
|
- VPN
|
||||||
|
- 192.168.1.0/24
|
||||||
|
- 10.0.0.1
|
||||||
|
|
||||||
- domain:
|
- domain:
|
||||||
- secure.example.com
|
- secure.example.com
|
||||||
|
|
|
@ -43,19 +43,19 @@ type Object struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectMatchingSubjectRules take a set of rules and select only the rules matching the subject constraints.
|
// selectMatchingSubjectRules take a set of rules and select only the rules matching the subject constraints.
|
||||||
func selectMatchingSubjectRules(rules []schema.ACLRule, subject Subject) []schema.ACLRule {
|
func selectMatchingSubjectRules(rules []schema.ACLRule, networks []schema.ACLNetwork, subject Subject) []schema.ACLRule {
|
||||||
selectedRules := []schema.ACLRule{}
|
selectedRules := []schema.ACLRule{}
|
||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
switch {
|
switch {
|
||||||
case len(rule.Subjects) > 0:
|
case len(rule.Subjects) > 0:
|
||||||
for _, subjectRule := range rule.Subjects {
|
for _, subjectRule := range rule.Subjects {
|
||||||
if isSubjectMatching(subject, subjectRule) && isIPMatching(subject.IP, rule.Networks) {
|
if isSubjectMatching(subject, subjectRule) && isIPMatching(subject.IP, rule.Networks, networks) {
|
||||||
selectedRules = append(selectedRules, rule)
|
selectedRules = append(selectedRules, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if isIPMatching(subject.IP, rule.Networks) {
|
if isIPMatching(subject.IP, rule.Networks, networks) {
|
||||||
selectedRules = append(selectedRules, rule)
|
selectedRules = append(selectedRules, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,8 @@ func selectMatchingObjectRules(rules []schema.ACLRule, object Object) []schema.A
|
||||||
return selectedRules
|
return selectedRules
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectMatchingRules(rules []schema.ACLRule, subject Subject, object Object) []schema.ACLRule {
|
func selectMatchingRules(rules []schema.ACLRule, networks []schema.ACLNetwork, subject Subject, object Object) []schema.ACLRule {
|
||||||
matchingRules := selectMatchingSubjectRules(rules, subject)
|
matchingRules := selectMatchingSubjectRules(rules, networks, subject)
|
||||||
return selectMatchingObjectRules(matchingRules, object)
|
return selectMatchingObjectRules(matchingRules, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ func (p *Authorizer) GetRequiredLevel(subject Subject, requestURL url.URL) Level
|
||||||
logging.Logger().Tracef("Check authorization of subject %s and url %s.",
|
logging.Logger().Tracef("Check authorization of subject %s and url %s.",
|
||||||
subject.String(), requestURL.String())
|
subject.String(), requestURL.String())
|
||||||
|
|
||||||
matchingRules := selectMatchingRules(p.configuration.Rules, subject, Object{
|
matchingRules := selectMatchingRules(p.configuration.Rules, p.configuration.Networks, subject, Object{
|
||||||
Domain: requestURL.Hostname(),
|
Domain: requestURL.Hostname(),
|
||||||
Path: requestURL.Path,
|
Path: requestURL.Path,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
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
|
||||||
|
|
||||||
|
@ -13,3 +15,14 @@ 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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -3,33 +3,70 @@ package authorization
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func selectMatchingNetworkGroups(networks []string, aclNetworks []schema.ACLNetwork) []schema.ACLNetwork {
|
||||||
|
selectedNetworkGroups := []schema.ACLNetwork{}
|
||||||
|
|
||||||
|
for _, network := range networks {
|
||||||
|
for _, n := range aclNetworks {
|
||||||
|
for _, ng := range n.Name {
|
||||||
|
if network == ng {
|
||||||
|
selectedNetworkGroups = append(selectedNetworkGroups, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedNetworkGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPAddressOrCIDR(ip net.IP, network string) bool {
|
||||||
|
switch {
|
||||||
|
case ip.String() == network:
|
||||||
|
return true
|
||||||
|
case strings.Contains(network, "/"):
|
||||||
|
return parseCIDR(ip, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCIDR(ip net.IP, network string) bool {
|
||||||
|
_, ipNet, err := net.ParseCIDR(network)
|
||||||
|
if err != nil {
|
||||||
|
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.
|
||||||
func isIPMatching(ip net.IP, networks []string) bool {
|
func isIPMatching(ip net.IP, networks []string, aclNetworks []schema.ACLNetwork) bool {
|
||||||
// If no network is provided in the rule, we match any network
|
// If no network is provided in the rule, we match any network
|
||||||
if len(networks) == 0 {
|
if len(networks) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matchingNetworkGroups := selectMatchingNetworkGroups(networks, aclNetworks)
|
||||||
|
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
if !strings.Contains(network, "/") {
|
if net.ParseIP(network) == nil && !strings.Contains(network, "/") {
|
||||||
if ip.String() == network {
|
for _, n := range matchingNetworkGroups {
|
||||||
return true
|
for _, network := range n.Networks {
|
||||||
|
if isIPAddressOrCIDR(ip, network) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if isIPAddressOrCIDR(ip, network) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ipNet, err := net.ParseCIDR(network)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// TODO(c.michaud): make sure the rule is valid at startup to
|
|
||||||
// to such a case here.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ipNet.Contains(ip) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,28 @@ import (
|
||||||
|
|
||||||
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{}))
|
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{}, testACLNetwork))
|
||||||
|
|
||||||
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"127.0.0.1"}))
|
assert.True(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"127.0.0.1"}, testACLNetwork))
|
||||||
assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"127.0.0.1"}))
|
assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"127.0.0.1"}, testACLNetwork))
|
||||||
assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"127.0.0.1"}))
|
assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"127.0.0.1"}, testACLNetwork))
|
||||||
|
|
||||||
assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"10.0.0.1"}))
|
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.0/8"}))
|
assert.False(t, isIPMatching(net.ParseIP("127.0.0.1"), []string{"10.0.0.0/8"}, testACLNetwork))
|
||||||
|
|
||||||
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"10.0.0.0/8"}))
|
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{"192.168.0.0/24", "10.0.0.0/8"}))
|
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"192.168.0.0/24", "10.0.0.0/8"}, testACLNetwork))
|
||||||
|
|
||||||
|
// 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{"localhost"}, testACLNetwork))
|
||||||
|
assert.False(t, isIPMatching(net.ParseIP("127.1"), []string{"localhost"}, testACLNetwork))
|
||||||
|
assert.False(t, isIPMatching(net.ParseIP("not-an-ip"), []string{"localhost"}, testACLNetwork))
|
||||||
|
|
||||||
|
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"}, testACLNetwork))
|
||||||
|
|
||||||
|
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"internal"}, testACLNetwork))
|
||||||
|
assert.True(t, isIPMatching(net.ParseIP("10.230.5.1"), []string{"192.168.0.0/24", "internal"}, testACLNetwork))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
// AccessControlConfiguration represents the configuration related to ACLs.
|
||||||
"fmt"
|
type AccessControlConfiguration struct {
|
||||||
"net"
|
DefaultPolicy string `mapstructure:"default_policy"`
|
||||||
"strings"
|
Networks []ACLNetwork `mapstructure:"networks"`
|
||||||
)
|
Rules []ACLRule `mapstructure:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACLNetwork represents one ACL network group entry; "weak" coerces a single value into slice.
|
||||||
|
type ACLNetwork struct {
|
||||||
|
Name []string `mapstructure:"name,weak"`
|
||||||
|
Networks []string `mapstructure:"networks"`
|
||||||
|
}
|
||||||
|
|
||||||
// ACLRule represents one ACL rule entry; "weak" coerces a single value into slice.
|
// ACLRule represents one ACL rule entry; "weak" coerces a single value into slice.
|
||||||
type ACLRule struct {
|
type ACLRule struct {
|
||||||
|
@ -14,61 +21,3 @@ type ACLRule struct {
|
||||||
Networks []string `mapstructure:"networks"`
|
Networks []string `mapstructure:"networks"`
|
||||||
Resources []string `mapstructure:"resources"`
|
Resources []string `mapstructure:"resources"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPolicyValid check if policy is valid.
|
|
||||||
func IsPolicyValid(policy string) bool {
|
|
||||||
return policy == denyPolicy || policy == "one_factor" || policy == "two_factor" || policy == "bypass"
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSubjectValid check if a subject is valid.
|
|
||||||
func IsSubjectValid(subject string) bool {
|
|
||||||
return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNetworkValid check if a network is valid.
|
|
||||||
func IsNetworkValid(network string) bool {
|
|
||||||
_, _, err := net.ParseCIDR(network)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validate an ACL Rule.
|
|
||||||
func (r *ACLRule) Validate(validator *StructValidator) {
|
|
||||||
if len(r.Domains) == 0 {
|
|
||||||
validator.Push(fmt.Errorf("Domain must be provided"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsPolicyValid(r.Policy) {
|
|
||||||
validator.Push(fmt.Errorf("A policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 i, network := range r.Networks {
|
|
||||||
if !IsNetworkValid(network) {
|
|
||||||
validator.Push(fmt.Errorf("Network %d must be a valid CIDR", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccessControlConfiguration represents the configuration related to ACLs.
|
|
||||||
type AccessControlConfiguration struct {
|
|
||||||
DefaultPolicy string `mapstructure:"default_policy"`
|
|
||||||
Rules []ACLRule `mapstructure:"rules"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validate the access control configuration.
|
|
||||||
func (acc *AccessControlConfiguration) Validate(validator *StructValidator) {
|
|
||||||
if acc.DefaultPolicy == "" {
|
|
||||||
acc.DefaultPolicy = denyPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsPolicyValid(acc.DefaultPolicy) {
|
|
||||||
validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,8 +4,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const denyPolicy = "deny"
|
|
||||||
|
|
||||||
const argon2id = "argon2id"
|
const argon2id = "argon2id"
|
||||||
|
|
||||||
// ProfileRefreshDisabled represents a value for refresh_interval that disables the check entirely.
|
// ProfileRefreshDisabled represents a value for refresh_interval that disables the check entirely.
|
||||||
|
|
|
@ -74,7 +74,7 @@ access_control:
|
||||||
resources:
|
resources:
|
||||||
- "^/deny-all.*$"
|
- "^/deny-all.*$"
|
||||||
subject: ["group:dev", "user:john"]
|
subject: ["group:dev", "user:john"]
|
||||||
policy: denied
|
policy: deny
|
||||||
|
|
||||||
# Rules applied to user 'harry'
|
# Rules applied to user 'harry'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
|
|
|
@ -74,7 +74,7 @@ access_control:
|
||||||
resources:
|
resources:
|
||||||
- "^/deny-all.*$"
|
- "^/deny-all.*$"
|
||||||
subject: ["group:dev", "user:john"]
|
subject: ["group:dev", "user:john"]
|
||||||
policy: denied
|
policy: deny
|
||||||
|
|
||||||
# Rules applied to user 'harry'
|
# Rules applied to user 'harry'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
|
|
|
@ -75,7 +75,7 @@ access_control:
|
||||||
resources:
|
resources:
|
||||||
- "^/deny-all.*$"
|
- "^/deny-all.*$"
|
||||||
subject: ["group:dev", "user:john"]
|
subject: ["group:dev", "user:john"]
|
||||||
policy: denied
|
policy: deny
|
||||||
|
|
||||||
# Rules applied to user 'harry'
|
# Rules applied to user 'harry'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
|
|
|
@ -75,7 +75,7 @@ access_control:
|
||||||
resources:
|
resources:
|
||||||
- "^/deny-all.*$"
|
- "^/deny-all.*$"
|
||||||
subject: ["group:dev", "user:john"]
|
subject: ["group:dev", "user:john"]
|
||||||
policy: denied
|
policy: deny
|
||||||
|
|
||||||
# Rules applied to user 'harry'
|
# Rules applied to user 'harry'
|
||||||
- domain: dev.example.com
|
- domain: dev.example.com
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsPolicyValid check if policy is valid.
|
||||||
|
func IsPolicyValid(policy string) bool {
|
||||||
|
return policy == denyPolicy || policy == "one_factor" || policy == "two_factor" || policy == "bypass"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubjectValid check if a subject is valid.
|
||||||
|
func IsSubjectValid(subject string) bool {
|
||||||
|
return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNetworkGroupValid check if a network group is valid.
|
||||||
|
func IsNetworkGroupValid(configuration schema.AccessControlConfiguration, network string) bool {
|
||||||
|
for _, networks := range configuration.Networks {
|
||||||
|
if !utils.IsStringInSlice(network, networks.Name) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNetworkValid check if a network is valid.
|
||||||
|
func IsNetworkValid(network string) bool {
|
||||||
|
if net.ParseIP(network) == nil {
|
||||||
|
_, _, err := net.ParseCIDR(network)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAccessControl validates access control configuration.
|
||||||
|
func ValidateAccessControl(configuration schema.AccessControlConfiguration, validator *schema.StructValidator) {
|
||||||
|
if !IsPolicyValid(configuration.DefaultPolicy) {
|
||||||
|
validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.Networks != nil {
|
||||||
|
for _, n := range configuration.Networks {
|
||||||
|
for _, networks := range n.Networks {
|
||||||
|
if !IsNetworkValid(networks) {
|
||||||
|
validator.Push(fmt.Errorf("Network %s from group %s must be a valid IP or CIDR", networks, n.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRules validates an ACL Rule configuration.
|
||||||
|
func ValidateRules(configuration schema.AccessControlConfiguration, validator *schema.StructValidator) {
|
||||||
|
for _, r := range configuration.Rules {
|
||||||
|
if len(r.Domains) == 0 {
|
||||||
|
validator.Push(fmt.Errorf("No access control rules have been defined"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if !IsNetworkValid(network) {
|
||||||
|
if !IsNetworkGroupValid(configuration, network) {
|
||||||
|
validator.Push(fmt.Errorf("Network %s is not a valid network or network group", network))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,6 +64,12 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
|
||||||
configuration.AccessControl.DefaultPolicy = "deny"
|
configuration.AccessControl.DefaultPolicy = "deny"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateAccessControl(configuration.AccessControl, validator)
|
||||||
|
|
||||||
|
if configuration.AccessControl.Rules != nil {
|
||||||
|
ValidateRules(configuration.AccessControl, validator)
|
||||||
|
}
|
||||||
|
|
||||||
ValidateSession(&configuration.Session, validator)
|
ValidateSession(&configuration.Session, validator)
|
||||||
|
|
||||||
if configuration.Regulation == nil {
|
if configuration.Regulation == nil {
|
||||||
|
|
|
@ -26,6 +26,7 @@ var validKeys = []string{
|
||||||
// Access Control Keys.
|
// Access Control Keys.
|
||||||
"access_control.rules",
|
"access_control.rules",
|
||||||
"access_control.default_policy",
|
"access_control.default_policy",
|
||||||
|
"access_control.networks",
|
||||||
|
|
||||||
// Session Keys.
|
// Session Keys.
|
||||||
"session.name",
|
"session.name",
|
||||||
|
@ -167,6 +168,8 @@ var specificErrorKeys = map[string]string{
|
||||||
"authentication_backend.file.hashing.parallelism": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password",
|
"authentication_backend.file.hashing.parallelism": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const denyPolicy = "deny"
|
||||||
|
|
||||||
const argon2id = "argon2id"
|
const argon2id = "argon2id"
|
||||||
const sha512 = "sha512"
|
const sha512 = "sha512"
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,11 @@ storage:
|
||||||
# resources.
|
# resources.
|
||||||
access_control:
|
access_control:
|
||||||
default_policy: deny
|
default_policy: deny
|
||||||
|
networks:
|
||||||
|
- name: Clients
|
||||||
|
networks:
|
||||||
|
- 192.168.240.202/32
|
||||||
|
- 192.168.240.203/32
|
||||||
rules:
|
rules:
|
||||||
- domain: secure.example.com
|
- domain: secure.example.com
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
|
@ -41,8 +46,7 @@ access_control:
|
||||||
- domain: secure.example.com
|
- domain: secure.example.com
|
||||||
policy: bypass
|
policy: bypass
|
||||||
networks:
|
networks:
|
||||||
- 192.168.240.202/32
|
- Clients
|
||||||
- 192.168.240.203/32
|
|
||||||
|
|
||||||
- domain: secure.example.com
|
- domain: secure.example.com
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
Loading…
Reference in New Issue