[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 valuespull/1597/head^2
parent
57c339bb96
commit
81e34d84de
|
@ -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"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue