diff options
46 files changed, 4898 insertions, 520 deletions
diff --git a/cmd/authelia-gen/cmd_docs_jsonschema.go b/cmd/authelia-gen/cmd_docs_jsonschema.go index f4f550ad5..37db76bb3 100644 --- a/cmd/authelia-gen/cmd_docs_jsonschema.go +++ b/cmd/authelia-gen/cmd_docs_jsonschema.go @@ -339,6 +339,20 @@ func getJSONSchemaOutputPath(cmd *cobra.Command, flag string) (dir, file string, func jsonschemaKoanfMapper(t reflect.Type) *jsonschema.Schema { switch t.String() { + case "[]*net.IPNet": + return &jsonschema.Schema{ + OneOf: []*jsonschema.Schema{ + { + Type: jsonschema.TypeString, + }, + { + Type: jsonschema.TypeArray, + Items: &jsonschema.Schema{ + Type: jsonschema.TypeString, + }, + }, + }, + } case "regexp.Regexp", "*regexp.Regexp": return &jsonschema.Schema{ Type: jsonschema.TypeString, diff --git a/cmd/authelia-gen/helpers.go b/cmd/authelia-gen/helpers.go index 4b6e33aa6..94c9b2cf5 100644 --- a/cmd/authelia-gen/helpers.go +++ b/cmd/authelia-gen/helpers.go @@ -170,6 +170,10 @@ func readTags(prefix string, t reflect.Type, envSkip, deprecatedSkip bool) (tags continue } case reflect.Slice: + if kind == reflect.Map { + tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, true, true)) + } + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, kind == reflect.Slice, kind == reflect.Map), field.Type.Elem(), envSkip, deprecatedSkip)...) } case reflect.Ptr: diff --git a/config.template.yml b/config.template.yml index c6231783d..7ee0e16eb 100644 --- a/config.template.yml +++ b/config.template.yml @@ -323,6 +323,22 @@ identity_validation: # disable_failure: false ## +## Definitions +## +## The definitions are used in other areas as reference points to reduce duplication. +## +# definitions: + ## The network definitions. + # network: + ## The name of the definition followed by the list of CIDR network addresses in this definition. + # internal: + # - '10.10.0.0/16' + # - '172.16.0.0/12' + # - '192.168.2.0/24' + # VPN: + # - '10.9.0.0/16' + +## ## Authentication Backend Provider Configuration ## ## Used for verifying user passwords and retrieve information such as email address and groups users belong to. @@ -622,14 +638,6 @@ identity_validation: ## resource if there is no policy to be applied to the user. # 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 applied to everyone # - domain: 'public.example.com' diff --git a/docs/content/configuration/definitions/_index.md b/docs/content/configuration/definitions/_index.md new file mode 100644 index 000000000..80a4cfde9 --- /dev/null +++ b/docs/content/configuration/definitions/_index.md @@ -0,0 +1,9 @@ +--- +title: "Definitions" +description: "Definitions Configuration" +summary: "" +date: 2024-10-19T23:15:30+11:00 +draft: false +images: [] +weight: 198000 +--- diff --git a/docs/content/configuration/definitions/introduction.md b/docs/content/configuration/definitions/introduction.md new file mode 100644 index 000000000..e2626567e --- /dev/null +++ b/docs/content/configuration/definitions/introduction.md @@ -0,0 +1,20 @@ +--- +title: "Definitions" +description: "Definitions Configuration" +summary: "Authelia allows configuring reusable definitions." +date: 2024-10-19T23:15:30+11:00 +draft: false +images: [] +weight: 199100 +toc: true +seo: + title: "" # custom title (optional) + description: "" # custom description (recommended) + canonical: "" # custom canonical URL (optional) + noindex: false # false (default) or true +--- + +## Definitions + +The definitions section controls several definitions which can be reused in other areas of the configuration rather than +repeating values elsewhere. diff --git a/docs/content/configuration/definitions/network.md b/docs/content/configuration/definitions/network.md new file mode 100644 index 000000000..b02157a3f --- /dev/null +++ b/docs/content/configuration/definitions/network.md @@ -0,0 +1,56 @@ +--- +title: "Network" +description: "Network Definitions Configuration" +summary: "Authelia allows configuring reusable network definitions." +date: 2024-10-19T23:15:30+11:00 +draft: false +images: [] +weight: 199100 +toc: true +seo: + title: "" # custom title (optional) + description: "" # custom description (recommended) + canonical: "" # custom canonical URL (optional) + noindex: false # false (default) or true +--- + +The network section configures named network lists. + +## Configuration + +{{< config-alert-example >}} + +```yaml {title="configuration.yml"} +definitions: + network: + network_name: + - '192.168.1.0/24' + - '192.168.2.20' + - '2001:db8::/32' + - '2001:db8:1234:5678::1' +``` + +## Options + +This section describes the individual configuration options. The configuration for this section is incredibly basic, +effectively it's key value pairs, where the key is the name used elsewhere in the configuration, and the value is a list +of network addresses. + +These definitions are used as [Access Control Networks](../security/access-control.md#networks) and +[OpenID Connect 1.0 Authorization Policy Networks](../identity-providers/openid-connect/provider.md#networks). + +### key + +The key is the name of the policy. In the example above, the key is `network_name` and is the value which must be used +in other areas of the configuration to reference it. + +### value + +{{< confkey type="string" syntax="network" required="yes" >}} + +The values which represent the CIDR notation of the IP's this definition applies to. In the example, the value is a list +which contains `192.168.1.0/24`, `192.168.2.20`, `2001:db8::/32`, and `2001:db8:1234:5678::1`. + +CIDR notation (e.g., `192.168.1.0/24`) represents a range of IP addresses. The number after the slash indicates how many +bits are used for the network portion. For example, `/24` means the first 24 bits are fixed, allowing the last 8 bits +to vary (giving you 256 possible addresses). A single IP address like `192.168.2.20` can be written as is or with `/32`. diff --git a/docs/content/configuration/prologue/common.md b/docs/content/configuration/prologue/common.md index 7cbc65d26..ecf6bebcc 100644 --- a/docs/content/configuration/prologue/common.md +++ b/docs/content/configuration/prologue/common.md @@ -220,6 +220,20 @@ Bad Example: domain_regex: "^(admin|secure)\.example\.com$" ``` +### Network + +We support a network syntax which unmarshalls strings into a network range. The string format uses the standard CIDR +notation and assumes a single host (adapted as /32 for IPv4 and /128 for IPv6) if the CIDR suffix is absent. + +| Example | CIDR | Range | +|:-----------------------------------------:|:------------------------------------------:|:---------------------------------------------------------------------------------:| +| 192.168.0.1 | 192.168.0.1/32 | 192.168.0.1 | +| 192.168.1.0/24 | 192.168.1.0/24 | 192.168.1.0 - 192.168.1.255 | +| 192.168.2.1/24 | 192.168.2.0/24 | 192.168.2.0 - 192.168.2.255 | +| 2001:db8:3333:4444:5555:6666:7777:8888 | 2001:db8:3333:4444:5555:6666:7777:8888/128 | 2001:db8:3333:4444:5555:6666:7777:8888 | +| 2001:db8:3333:4400::/56 | 2001:db8:3333:4400::/56 | 2001:0db8:3333:4400:0000:0000:0000:0000 - 2001:0db8:3333:44ff:ffff:ffff:ffff:ffff | +| 2001:db8:3333:4444:5555:6666:7777:8888/56 | 2001:db8:3333:4400::/56 | 2001:0db8:3333:4400:0000:0000:0000:0000 - 2001:0db8:3333:44ff:ffff:ffff:ffff:ffff | + ## Structures The following represent common data structures used within the configuration which have specific requirements that are diff --git a/docs/content/configuration/security/access-control.md b/docs/content/configuration/security/access-control.md index d67ebe271..e8e41f992 100644 --- a/docs/content/configuration/security/access-control.md +++ b/docs/content/configuration/security/access-control.md @@ -35,12 +35,6 @@ Some of the values within this page can automatically be replaced with documenta ```yaml {title="configuration.yml"} access_control: default_policy: 'deny' - networks: - - name: 'internal' - networks: - - '10.0.0.0/8' - - '172.16.0.0/12' - - '192.168.0.0/18' rules: - domain: 'private.{{< sitevar name="domain" nojs="example.com" >}}' domain_regex: '^(\d+\-)?priv-img\.{{< sitevar name="domain" format="regex" nojs="example\.com" >}}$' @@ -85,21 +79,6 @@ Authelia at all for performance reasons. See the [policies] section for more information. -### networks (global) - -{{< confkey type="list" required="no" >}} - -The main/global networks section contains a list of networks with a name label that can be reused in the -[rules](#networks) section instead of redefining the same networks over and over again. This additionally makes -complicated network related configuration a lot cleaner and easier to read. - -This section has two options, `name` and `networks`. Where the `networks` section is a list of IP addresses in CIDR -notation and where `name` is a friendly name to label the collection of networks for reuse in the [networks] section of -the [rules] section below. - -This configuration option *does nothing* by itself, it's only useful if you use these aliases in the [rules](#networks) -section below. - ### rules {{< confkey type="list" required="no" >}} @@ -366,13 +345,13 @@ access_control: #### networks -{{< confkey type="list(string)" required="no" >}} +{{< confkey type="list(string)" syntax="network" required="no" >}} -This criteria is a list of values which can be an IP Address, network address range in CIDR notation, or an alias from -the [global](#networks-global) section. It matches against the first address in the `X-Forwarded-For` header, or if there -are none it will fall back to the IP address of the packet TCP source IP address. For this reason it's important for you -to configure the proxy server correctly in order to accurately match requests with this criteria. *__Note:__ you may -combine CIDR networks with the alias rules as you please.* +These criteria consist of a list of values which can be an IP Address, network address range in CIDR notation, or a named +[Network Definition](../definitions/network.md). It matches against the first address in the `X-Forwarded-For` header, +or if there are none it will fall back to the IP address of the packet TCP source IP address. For this reason, it's +important for you to configure the proxy server correctly to accurately match requests with these criteria. +*__Note:__ you may combine CIDR networks with the alias rules as you please.* The main use case for this criteria is adjust the security requirements of a resource based on the location of a user. You can theoretically consider a specific network to be one of the factors involved in authentication, you can deny @@ -394,14 +373,14 @@ for administrators to tune the security to their specific needs if desired. rules in this list are effectively the same rule just expressed in different ways.* ```yaml {title="configuration.yml"} -access_control: - default_policy: 'two_factor' - networks: - - name: 'internal' - networks: +definitions: + network: + internal: - '10.0.0.0/8' - '172.16.0.0/12' - '192.168.0.0/18' +access_control: + default_policy: 'two_factor' rules: - domain: 'secure.{{< sitevar name="domain" nojs="example.com" >}}' policy: 'one_factor' @@ -630,15 +609,15 @@ alphanumeric (including spaces). Here is a detailed example of an example access control section: ```yaml {title="configuration.yml"} +definitions: + network: + internal: + - '10.10.0.0/16' + - '192.168.2.0/24' + vpn: '10.9.0.0/16' + access_control: 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: - domain: 'public.{{< sitevar name="domain" nojs="example.com" >}}' policy: 'bypass' @@ -652,7 +631,7 @@ access_control: policy: 'one_factor' networks: - 'internal' - - 'VPN' + - 'vpn' - '192.168.1.0/24' - '10.0.0.1' diff --git a/docs/layouts/shortcodes/confkey.html b/docs/layouts/shortcodes/confkey.html index 933238a1b..458a23c87 100644 --- a/docs/layouts/shortcodes/confkey.html +++ b/docs/layouts/shortcodes/confkey.html @@ -12,6 +12,7 @@ {{- with $structure }}{{ $ref = . }}{{ end }} {{- else if eq $syntax "duration" }}{{ $ref = "duration" }} {{- else if eq $syntax "address" }}{{ $ref = "address" }} +{{- else if eq $syntax "network" }}{{ $ref = "network" }} {{- end }} {{- end }} <div class="mb-3"> diff --git a/docs/static/schemas/v4.39/json-schema/configuration.json b/docs/static/schemas/v4.39/json-schema/configuration.json new file mode 100644 index 000000000..128236027 --- /dev/null +++ b/docs/static/schemas/v4.39/json-schema/configuration.json @@ -0,0 +1,3421 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.authelia.com/schemas/v4.39/json-schema/configuration.json", + "$ref": "#/$defs/Configuration", + "$defs": { + "AccessControl": { + "properties": { + "default_policy": { + "type": "string", + "enum": [ + "deny", + "one_factor", + "two_factor" + ], + "title": "Default Authorization Policy", + "description": "The default policy applied to all authorization requests unrelated to OpenID Connect 1.0.", + "default": "deny" + }, + "networks": { + "items": { + "$ref": "#/$defs/AccessControlNetwork" + }, + "type": "array", + "title": "Named Networks", + "description": "The list of named networks which can be reused in any ACL rule." + }, + "rules": { + "items": { + "$ref": "#/$defs/AccessControlRule" + }, + "type": "array", + "title": "Rules List", + "description": "The list of ACL rules to enumerate for requests." + } + }, + "additionalProperties": false, + "type": "object", + "description": "AccessControl represents the configuration related to ACLs." + }, + "AccessControlNetwork": { + "properties": { + "name": { + "type": "string", + "title": "Network Name", + "description": "The name of this network to be used in the networks section of the rules section." + }, + "networks": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "title": "Networks", + "description": "The remote IP's or network ranges in CIDR notation that this rule applies to." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "networks" + ], + "description": "AccessControlNetwork represents one ACL network group entry." + }, + "AccessControlRule": { + "oneOf": [ + { + "required": [ + "domain" + ], + "title": "Domain" + }, + { + "required": [ + "domain_regex" + ], + "title": "Domain Regex" + } + ], + "properties": { + "domain": { + "$ref": "#/$defs/AccessControlRuleDomains", + "title": "Domain Literals", + "description": "The literal domains to match the domain against that this rule applies to." + }, + "domain_regex": { + "$ref": "#/$defs/AccessControlRuleRegex", + "title": "Domain Regex Patterns", + "description": "The regex patterns to match the domain against that this rule applies to." + }, + "policy": { + "type": "string", + "enum": [ + "bypass", + "deny", + "one_factor", + "two_factor" + ], + "title": "Rule Policy", + "description": "The policy this rule applies when all criteria match." + }, + "subject": { + "$ref": "#/$defs/AccessControlRuleSubjects", + "title": "AccessControlRuleSubjects", + "description": "The users or groups that this rule applies to." + }, + "networks": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "title": "Networks", + "description": "The remote IP's, network ranges in CIDR notation, or network definition names that this rule applies to." + }, + "resources": { + "$ref": "#/$defs/AccessControlRuleRegex", + "title": "Resources or Paths", + "description": "The regex patterns to match the resource paths that this rule applies to." + }, + "methods": { + "$ref": "#/$defs/AccessControlRuleMethods", + "description": "The list of request methods this rule applies to." + }, + "query": { + "items": { + "items": { + "$ref": "#/$defs/AccessControlRuleQuery" + }, + "type": "array" + }, + "type": "array", + "title": "Query Rules", + "description": "The list of query parameter rules this rule applies to." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "policy" + ], + "description": "AccessControlRule represents one ACL rule entry." + }, + "AccessControlRuleDomains": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + } + ] + }, + "AccessControlRuleMethods": { + "oneOf": [ + { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "PATCH", + "DELETE", + "TRACE", + "CONNECT", + "OPTIONS", + "COPY", + "LOCK", + "MKCOL", + "MOVE", + "PROPFIND", + "PROPPATCH", + "UNLOCK" + ] + }, + { + "items": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "PATCH", + "DELETE", + "TRACE", + "CONNECT", + "OPTIONS", + "COPY", + "LOCK", + "MKCOL", + "MOVE", + "PROPFIND", + "PROPPATCH", + "UNLOCK" + ] + }, + "type": "array", + "uniqueItems": true + } + ] + }, + "AccessControlRuleQuery": { + "properties": { + "operator": { + "type": "string", + "enum": [ + "equal", + "not equal", + "present", + "absent", + "pattern", + "not pattern" + ], + "title": "Operator", + "description": "The list of query parameter rules this rule applies to." + }, + "key": { + "type": "string", + "title": "Key", + "description": "The Query Parameter key this rule applies to." + }, + "value": { + "title": "Value", + "description": "The Query Parameter value for this rule." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ], + "description": "AccessControlRuleQuery represents the ACL query criteria." + }, + "AccessControlRuleRegex": { + "oneOf": [ + { + "type": "string", + "format": "regex" + }, + { + "items": { + "type": "string", + "format": "regex" + }, + "type": "array", + "uniqueItems": true + } + ] + }, + "AccessControlRuleSubjects": { + "oneOf": [ + { + "type": "string", + "pattern": "^(user|group|oauth2:client):.+$" + }, + { + "items": { + "type": "string", + "pattern": "^(user|group|oauth2:client):.+$" + }, + "type": "array" + }, + { + "items": { + "items": { + "type": "string", + "pattern": "^(user|group|oauth2:client):.+$" + }, + "type": "array" + }, + "type": "array", + "uniqueItems": true + } + ] + }, + "AddressLDAP": { + "type": "string", + "pattern": "^((ldaps?:\\/\\/)?([^:\\/]*(:\\d+)|[^:\\/]+(:\\d+)?)?|ldapi:\\/\\/(\\/[^?\\n]+)?)$", + "format": "uri" + }, + "AddressSMTP": { + "type": "string", + "pattern": "^((smtp|submissions?):\\/\\/)?([^:\\/]*(:\\d+)|[^:\\/]+(:\\d+)?)?$", + "format": "uri" + }, + "AddressTCP": { + "type": "string", + "pattern": "^((tcp[46]?:\\/\\/)?([^:\\/]*(:\\d+)|[^:\\/]+(:\\d+)?)(\\/.*)?|unix:\\/\\/\\/[^?\\n]+(\\?(umask=[0-7]{3,4}|path=[a-z]+)(\u0026(umask=[0-7]{3,4}|path=[a-zA-Z0-9.~_-]+))?)?)$", + "format": "uri" + }, + "AddressUDP": { + "type": "string", + "pattern": "^(udp[46]?:\\/\\/)?([^:\\/]*(:\\d+)|[^:\\/]+(:\\d+)?)(\\/.*)?$", + "format": "uri" + }, + "AuthenticationBackend": { + "properties": { + "password_reset": { + "$ref": "#/$defs/AuthenticationBackendPasswordReset", + "title": "Password Reset", + "description": "Allows configuration of the password reset behaviour." + }, + "refresh_interval": { + "$ref": "#/$defs/RefreshIntervalDuration", + "title": "Refresh Interval", + "description": "How frequently the user details are refreshed from the backend." + }, + "file": { + "$ref": "#/$defs/AuthenticationBackendFile", + "title": "File Backend", + "description": "The file authentication backend configuration." + }, + "ldap": { + "$ref": "#/$defs/AuthenticationBackendLDAP", + "title": "LDAP Backend", + "description": "The LDAP authentication backend configuration." + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackend represents the configuration related to the authentication backend." + }, + "AuthenticationBackendFile": { + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "The file path to the user database." + }, + "watch": { + "type": "boolean", + "title": "Watch", + "description": "Enables watching the file for external changes and dynamically reloading the database.", + "default": false + }, + "password": { + "$ref": "#/$defs/AuthenticationBackendFilePassword", + "title": "Password Options", + "description": "Allows configuration of the password hashing options when the user passwords are changed directly by Authelia." + }, + "search": { + "$ref": "#/$defs/AuthenticationBackendFileSearch", + "title": "Search", + "description": "Configures the user searching behaviour." + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFile represents the configuration related to file-based backend." + }, + "AuthenticationBackendFilePassword": { + "properties": { + "algorithm": { + "type": "string", + "enum": [ + "argon2", + "sha2crypt", + "pbkdf2", + "bcrypt", + "scrypt" + ], + "title": "Algorithm", + "description": "The password hashing algorithm to use.", + "default": "argon2" + }, + "argon2": { + "$ref": "#/$defs/AuthenticationBackendFilePasswordArgon2", + "title": "Argon2", + "description": "Configure the Argon2 password hashing parameters." + }, + "sha2crypt": { + "$ref": "#/$defs/AuthenticationBackendFilePasswordSHA2Crypt", + "title": "SHA2Crypt", + "description": "Configure the SHA2Crypt password hashing parameters." + }, + "pbkdf2": { + "$ref": "#/$defs/AuthenticationBackendFilePasswordPBKDF2", + "title": "PBKDF2", + "description": "Configure the PBKDF2 password hashing parameters." + }, + "bcrypt": { + "$ref": "#/$defs/AuthenticationBackendFilePasswordBCrypt", + "title": "BCrypt", + "description": "Configure the BCrypt password hashing parameters." + }, + "scrypt": { + "$ref": "#/$defs/AuthenticationBackendFilePasswordSCrypt", + "title": "SCrypt", + "description": "Configure the SCrypt password hashing parameters." + }, + "iterations": { + "type": "integer", + "title": "Iterations", + "description": "Deprecated: Use individual password options instead.", + "deprecated": true + }, + "memory": { + "type": "integer", + "title": "Memory", + "description": "Deprecated: Use individual password options instead.", + "deprecated": true + }, + "parallelism": { + "type": "integer", + "title": "Parallelism", + "description": "Deprecated: Use individual password options instead.", + "deprecated": true + }, + "key_length": { + "type": "integer", + "title": "Key Length", + "description": "Deprecated: Use individual password options instead.", + "deprecated": true + }, + "salt_length": { + "type": "integer", + "title": "Salt Length", + "description": "Deprecated: Use individual password options instead.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFilePassword represents the configuration related to password hashing." + }, + "AuthenticationBackendFilePasswordArgon2": { + "properties": { + "variant": { + "type": "string", + "enum": [ + "argon2id", + "argon2i", + "argon2d" + ], + "title": "Variant", + "description": "The Argon2 variant to be used.", + "default": "argon2id" + }, + "iterations": { + "type": "integer", + "title": "Iterations", + "description": "The number of Argon2 iterations (parameter t) to be used.", + "default": 3 + }, + "memory": { + "type": "integer", + "maximum": 4294967295, + "minimum": 8, + "title": "Memory", + "description": "The Argon2 amount of memory in kibibytes (parameter m) to be used.", + "default": 65536 + }, + "parallelism": { + "type": "integer", + "maximum": 16777215, + "minimum": 1, + "title": "Parallelism", + "description": "The Argon2 degree of parallelism (parameter p) to be used.", + "default": 4 + }, + "key_length": { + "type": "integer", + "maximum": 2147483647, + "minimum": 4, + "title": "Key Length", + "description": "The Argon2 key output length.", + "default": 32 + }, + "salt_length": { + "type": "integer", + "maximum": 2147483647, + "minimum": 1, + "title": "Salt Length", + "description": "The Argon2 salt length.", + "default": 16 + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFilePasswordArgon2 represents the argon2 hashing settings." + }, + "AuthenticationBackendFilePasswordBCrypt": { + "properties": { + "variant": { + "type": "string", + "enum": [ + "standard", + "sha256" + ], + "title": "Variant", + "description": "The BCrypt variant to be used.", + "default": "standard" + }, + "cost": { + "type": "integer", + "maximum": 31, + "minimum": 10, + "title": "Cost", + "description": "The BCrypt cost to be used.", + "default": 12 + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFilePasswordBCrypt represents the bcrypt hashing settings." + }, + "AuthenticationBackendFilePasswordPBKDF2": { + "properties": { + "variant": { + "type": "string", + "enum": [ + "sha1", + "sha224", + "sha256", + "sha384", + "sha512" + ], + "title": "Variant", + "description": "The PBKDF2 variant to be used.", + "default": "sha512" + }, + "iterations": { + "type": "integer", + "maximum": 2147483647, + "minimum": 100000, + "title": "Iterations", + "description": "The PBKDF2 iterations to be used.", + "default": 310000 + }, + "salt_length": { + "type": "integer", + "maximum": 2147483647, + "minimum": 8, + "title": "Salt Length", + "description": "The PBKDF2 salt length to be used.", + "default": 16 + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFilePasswordPBKDF2 represents the PBKDF2 hashing settings." + }, + "AuthenticationBackendFilePasswordSCrypt": { + "properties": { + "iterations": { + "type": "integer", + "maximum": 58, + "minimum": 1, + "title": "Iterations", + "description": "The SCrypt iterations to be used.", + "default": 16 + }, + "block_size": { + "type": "integer", + "maximum": 36028797018963967, + "minimum": 1, + "title": "Key Length", + "description": "The SCrypt block size to be used.", + "default": 8 + }, + "parallelism": { + "type": "integer", + "maximum": 1073741823, + "minimum": 1, + "title": "Key Length", + "description": "The SCrypt parallelism factor to be used.", + "default": 1 + }, + "key_length": { + "type": "integer", + "maximum": 137438953440, + "minimum": 1, + "title": "Key Length", + "description": "The SCrypt key length to be used.", + "default": 32 + }, + "salt_length": { + "type": "integer", + "maximum": 1024, + "minimum": 8, + "title": "Salt Length", + "description": "The SCrypt salt length to be used.", + "default": 16 + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFilePasswordSCrypt represents the scrypt hashing settings." + }, + "AuthenticationBackendFilePasswordSHA2Crypt": { + "properties": { + "variant": { + "type": "string", + "enum": [ + "sha256", + "sha512" + ], + "title": "Variant", + "description": "The SHA2Crypt variant to be used.", + "default": "sha512" + }, + "iterations": { + "type": "integer", + "maximum": 999999999, + "minimum": 1000, + "title": "Iterations", + "description": "The SHA2Crypt iterations (parameter rounds) to be used.", + "default": 50000 + }, + "salt_length": { + "type": "integer", + "maximum": 16, + "minimum": 1, + "title": "Salt Length", + "description": "The SHA2Crypt salt length to be used.", + "default": 16 + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFilePasswordSHA2Crypt represents the sha2crypt hashing settings." + }, + "AuthenticationBackendFileSearch": { + "properties": { + "email": { + "type": "boolean", + "title": "Email Searching", + "description": "Allows users to either use their username or their configured email as a username.", + "default": false + }, + "case_insensitive": { + "type": "boolean", + "title": "Case Insensitive Searching", + "description": "Allows usernames to be any case during the search.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendFileSearch represents the configuration related to file-based backend searching." + }, + "AuthenticationBackendLDAP": { + "properties": { + "address": { + "$ref": "#/$defs/AddressLDAP", + "title": "Address", + "description": "The address of the LDAP directory server." + }, + "implementation": { + "type": "string", + "enum": [ + "custom", + "activedirectory", + "rfc2307bis", + "freeipa", + "lldap", + "glauth" + ], + "title": "Implementation", + "description": "The implementation which mostly decides the default values.", + "default": "custom" + }, + "timeout": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Timeout", + "description": "The LDAP directory server connection timeout." + }, + "start_tls": { + "type": "boolean", + "title": "StartTLS", + "description": "Enables the use of StartTLS.", + "default": false + }, + "tls": { + "$ref": "#/$defs/TLS", + "title": "TLS", + "description": "The LDAP directory server TLS connection properties." + }, + "base_dn": { + "type": "string", + "title": "Base DN", + "description": "The base for all directory server operations." + }, + "additional_users_dn": { + "type": "string", + "title": "Additional User Base", + "description": "The base in addition to the Base DN for all directory server operations for users." + }, + "users_filter": { + "type": "string", + "title": "Users Filter", + "description": "The LDAP filter used to search for user objects." + }, + "additional_groups_dn": { + "type": "string", + "title": "Additional Group Base", + "description": "The base in addition to the Base DN for all directory server operations for groups." + }, + "groups_filter": { + "type": "string", + "title": "Groups Filter", + "description": "The LDAP filter used to search for group objects." + }, + "group_search_mode": { + "type": "string", + "enum": [ + "filter", + "memberof" + ], + "title": "Groups Search Mode", + "description": "The LDAP group search mode used to search for group objects.", + "default": "filter" + }, + "attributes": { + "$ref": "#/$defs/AuthenticationBackendLDAPAttributes" + }, + "permit_referrals": { + "type": "boolean", + "title": "Permit Referrals", + "description": "Enables chasing LDAP referrals.", + "default": false + }, + "permit_unauthenticated_bind": { + "type": "boolean", + "title": "Permit Unauthenticated Bind", + "description": "Enables omission of the password to perform an unauthenticated bind.", + "default": false + }, + "permit_feature_detection_failure": { + "type": "boolean", + "title": "Permit Feature Detection Failure", + "description": "Enables failures when detecting directory server features using the Root DSE lookup.", + "default": false + }, + "user": { + "type": "string", + "title": "User", + "description": "The user distinguished name for LDAP binding." + }, + "password": { + "type": "string", + "title": "Password", + "description": "The password for LDAP authenticated binding." + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendLDAP represents the configuration related to LDAP server." + }, + "AuthenticationBackendLDAPAttributes": { + "properties": { + "distinguished_name": { + "type": "string", + "title": "Attribute: Distinguished Name", + "description": "The directory server attribute which contains the distinguished name for all objects." + }, + "username": { + "type": "string", + "title": "Attribute: User Username", + "description": "The directory server attribute which contains the username for all users." + }, + "display_name": { + "type": "string", + "title": "Attribute: User Display Name", + "description": "The directory server attribute which contains the display name for all users." + }, + "mail": { + "type": "string", + "title": "Attribute: User Mail", + "description": "The directory server attribute which contains the mail address for all users and groups." + }, + "MemberOf": { + "type": "string", + "title": "Attribute: Member Of", + "description": "The directory server attribute which contains the objects that an object is a member of." + }, + "group_name": { + "type": "string", + "title": "Attribute: Group Name", + "description": "The directory server attribute which contains the group name for all groups." + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendLDAPAttributes represents the configuration related to LDAP server attributes." + }, + "AuthenticationBackendPasswordReset": { + "properties": { + "disable": { + "type": "boolean", + "title": "Disable", + "description": "Disables the Password Reset option.", + "default": false + }, + "custom_url": { + "type": "string", + "format": "uri", + "title": "Custom URL", + "description": "Disables the internal Password Reset option and instead redirects users to this specified URL." + } + }, + "additionalProperties": false, + "type": "object", + "description": "AuthenticationBackendPasswordReset represents the configuration related to password reset functionality." + }, + "Configuration": { + "properties": { + "theme": { + "type": "string", + "enum": [ + "auto", + "light", + "dark", + "grey" + ], + "title": "Theme Name", + "description": "The name of the theme to apply to the web UI.", + "default": "light" + }, + "certificates_directory": { + "type": "string", + "title": "Certificates Directory Path", + "description": "The path to a directory which is used to determine the certificates that are trusted." + }, + "default_2fa_method": { + "type": "string", + "enum": [ + "totp", + "webauthn", + "mobile_push" + ], + "title": "Default 2FA method", + "description": "When a user logs in for the first time this is the 2FA method configured for them." + }, + "log": { + "$ref": "#/$defs/Log", + "title": "Log", + "description": "Logging Configuration." + }, + "identity_providers": { + "$ref": "#/$defs/IdentityProviders", + "title": "Identity Providers", + "description": "Identity Providers Configuration." + }, + "authentication_backend": { + "$ref": "#/$defs/AuthenticationBackend", + "title": "Authentication Backend", + "description": "Authentication Backend Configuration." + }, + "session": { + "$ref": "#/$defs/Session", + "title": "Session", + "description": "Session Configuration." + }, + "totp": { + "$ref": "#/$defs/TOTP", + "title": "TOTP", + "description": "Time-based One-Time Password Configuration." + }, + "duo_api": { + "$ref": "#/$defs/DuoAPI", + "title": "Duo API", + "description": "Duo API Configuration." + }, + "access_control": { + "$ref": "#/$defs/AccessControl", + "title": "Access Control", + "description": "Access Control Configuration." + }, + "ntp": { + "$ref": "#/$defs/NTP", + "title": "NTP", + "description": "Network Time Protocol Configuration." + }, + "regulation": { + "$ref": "#/$defs/Regulation", + "title": "Regulation", + "description": "Regulation Configuration." + }, + "storage": { + "$ref": "#/$defs/Storage", + "title": "Storage", + "description": "Storage Configuration." + }, + "notifier": { + "$ref": "#/$defs/Notifier", + "title": "Notifier", + "description": "Notifier Configuration." + }, + "server": { + "$ref": "#/$defs/Server", + "title": "Server", + "description": "Server Configuration." + }, + "telemetry": { + "$ref": "#/$defs/Telemetry", + "title": "Telemetry", + "description": "Telemetry Configuration." + }, + "webauthn": { + "$ref": "#/$defs/WebAuthn", + "title": "WebAuthn", + "description": "WebAuthn Configuration." + }, + "password_policy": { + "$ref": "#/$defs/PasswordPolicy", + "title": "Password Policy", + "description": "Password Policy Configuration." + }, + "privacy_policy": { + "$ref": "#/$defs/PrivacyPolicy", + "title": "Privacy Policy", + "description": "Privacy Policy Configuration." + }, + "identity_validation": { + "$ref": "#/$defs/IdentityValidation", + "title": "Identity Validation", + "description": "Identity Validation Configuration." + }, + "definitions": { + "$ref": "#/$defs/Definitions", + "title": "Definitions", + "description": "Definitions for items reused elsewhere in the configuration." + }, + "default_redirection_url": { + "type": "string", + "format": "uri", + "title": "The default redirection URL", + "description": "Deprecated: Use the session cookies option with the same name instead.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "Configuration object extracted from YAML configuration file." + }, + "Definitions": { + "properties": { + "network": { + "patternProperties": { + ".*": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object", + "title": "Network Definitions", + "description": "Networks CIDR ranges that can be utilized elsewhere in the configuration." + } + }, + "additionalProperties": false, + "type": "object" + }, + "DuoAPI": { + "properties": { + "disable": { + "type": "boolean", + "title": "Disable", + "description": "Disable the Duo API integration.", + "default": false + }, + "hostname": { + "type": "string", + "format": "hostname", + "title": "Hostname", + "description": "The Hostname provided by your Duo API dashboard." + }, + "integration_key": { + "type": "string", + "title": "Integration Key", + "description": "The Integration Key provided by your Duo API dashboard." + }, + "secret_key": { + "type": "string", + "title": "Secret Key", + "description": "The Secret Key provided by your Duo API dashboard." + }, + "enable_self_enrollment": { + "type": "boolean", + "title": "Enable Self Enrollment", + "description": "Enable the Self Enrollment flow.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "DuoAPI represents the configuration related to Duo API." + }, + "IdentityProviders": { + "properties": { + "oidc": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnect" + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProviders represents the Identity Providers configuration for Authelia." + }, + "IdentityProvidersOpenIDConnect": { + "properties": { + "hmac_secret": { + "type": "string", + "title": "HMAC Secret", + "description": "The HMAC Secret used to sign Access Tokens." + }, + "jwks": { + "items": { + "$ref": "#/$defs/JWK" + }, + "type": "array", + "title": "Issuer JSON Web Keys", + "description": "The JWK's which are to be used to sign various objects like ID Tokens." + }, + "enable_client_debug_messages": { + "type": "boolean", + "title": "Enable Client Debug Messages", + "description": "Enables additional debug messages for clients.", + "default": false + }, + "minimum_parameter_entropy": { + "type": "integer", + "minimum": -1, + "title": "Minimum Parameter Entropy", + "description": "The minimum entropy of the nonce parameter.", + "default": 8 + }, + "enforce_pkce": { + "type": "string", + "enum": [ + "public_clients_only", + "never", + "always" + ], + "title": "Enforce PKCE", + "description": "Controls enforcement of the use of Proof Key for Code Exchange on all clients.", + "default": "public_clients_only" + }, + "enable_pkce_plain_challenge": { + "type": "boolean", + "title": "Enable PKCE Plain Challenge", + "description": "Enables use of the discouraged plain Proof Key for Code Exchange challenges.", + "default": false + }, + "enable_jwt_access_token_stateless_introspection": { + "type": "boolean", + "title": "Enable JWT Access Token Stateless Introspection", + "description": "Allows the use of stateless introspection of JWT Access Tokens which is not recommended." + }, + "discovery_signed_response_alg": { + "type": "string", + "enum": [ + "none", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Discovery Response Signing Algorithm", + "description": "The Algorithm this provider uses to sign the Discovery and Metadata Document responses.", + "default": "none" + }, + "discovery_signed_response_key_id": { + "type": "string", + "title": "Discovery Response Signing Key ID", + "description": "The Key ID this provider uses to sign the Discovery and Metadata Document responses (overrides the 'discovery_signed_response_alg')." + }, + "require_pushed_authorization_requests": { + "type": "boolean", + "title": "Require Pushed Authorization Requests", + "description": "Requires Pushed Authorization Requests for all clients for this Issuer." + }, + "cors": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectCORS", + "title": "CORS", + "description": "Configuration options for Cross-Origin Request Sharing." + }, + "clients": { + "items": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectClient" + }, + "type": "array", + "title": "Clients", + "description": "OpenID Connect 1.0 clients registry." + }, + "authorization_policies": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectPolicy" + } + }, + "type": "object", + "title": "Authorization Policies", + "description": "Custom client authorization policies." + }, + "lifespans": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespans", + "title": "Lifespans", + "description": "Token lifespans configuration." + }, + "issuer_certificate_chain": { + "$ref": "#/$defs/X509CertificateChain", + "title": "Issuer Certificate Chain", + "description": "The Issuer Certificate Chain with an RSA Public Key used to sign ID Tokens.", + "deprecated": true + }, + "issuer_private_key": { + "type": "string", + "pattern": "^-{5}(BEGIN (RSA )?PRIVATE KEY-{5}\\n([a-zA-Z0-9\\/+]{1,64}\\n)+([a-zA-Z0-9\\/+]{1,64}[=]{0,2})\\n-{5}END (RSA )?PRIVATE KEY-{5}\\n?)+$", + "title": "Issuer Private Key", + "description": "The Issuer Private Key with an RSA Private Key used to sign ID Tokens.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnect represents the configuration for OpenID Connect 1.0." + }, + "IdentityProvidersOpenIDConnectCORS": { + "properties": { + "endpoints": { + "items": { + "type": "string", + "enum": [ + "authorization", + "pushed-authorization-request", + "token", + "introspection", + "revocation", + "userinfo" + ] + }, + "type": "array", + "uniqueItems": true, + "title": "Endpoints", + "description": "List of endpoints to enable CORS handling for." + }, + "allowed_origins": { + "items": { + "type": "string", + "format": "uri" + }, + "type": "array", + "title": "Allowed Origins", + "description": "List of arbitrary allowed origins for CORS requests." + }, + "allowed_origins_from_client_redirect_uris": { + "type": "boolean", + "title": "Allowed Origins From Client Redirect URIs", + "description": "Automatically include the redirect URIs from the registered clients.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnectCORS represents an OpenID Connect 1.0 CORS config." + }, + "IdentityProvidersOpenIDConnectClient": { + "properties": { + "client_id": { + "type": "string", + "minLength": 1, + "title": "Client ID", + "description": "The Client ID." + }, + "client_name": { + "type": "string", + "title": "Client Name", + "description": "The Client Name displayed to End-Users." + }, + "client_secret": { + "$ref": "#/$defs/PasswordDigest", + "title": "Client Secret", + "description": "The Client Secret for Client Authentication." + }, + "sector_identifier_uri": { + "type": "string", + "format": "uri", + "title": "Sector Identifier URI", + "description": "The Client Sector Identifier URI for Privacy Isolation via Pairwise subject types." + }, + "public": { + "type": "boolean", + "title": "Public", + "description": "Enables the Public Client Type.", + "default": false + }, + "redirect_uris": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectClientURIs", + "title": "Redirect URIs", + "description": "List of whitelisted redirect URIs." + }, + "request_uris": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectClientURIs", + "title": "Request URIs", + "description": "List of whitelisted request URIs." + }, + "audience": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true, + "title": "Audience", + "description": "List of authorized audiences." + }, + "scopes": { + "items": { + "type": "string", + "enum": [ + "openid", + "offline_access", + "groups", + "email", + "profile", + "authelia.bearer.authz" + ] + }, + "type": "array", + "uniqueItems": true, + "title": "Scopes", + "description": "The Scopes this client is allowed request and be granted." + }, + "grant_types": { + "items": { + "type": "string", + "enum": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ] + }, + "type": "array", + "uniqueItems": true, + "title": "Grant Types", + "description": "The Grant Types this client is allowed to use for the protected endpoints." + }, + "response_types": { + "items": { + "type": "string", + "enum": [ + "code", + "id_token token", + "id_token", + "token", + "code token", + "code id_token", + "code id_token token" + ] + }, + "type": "array", + "uniqueItems": true, + "title": "Response Types", + "description": "The Response Types the client is authorized to request." + }, + "response_modes": { + "items": { + "type": "string", + "enum": [ + "form_post", + "form_post.jwt", + "query", + "query.jwt", + "fragment", + "fragment.jwt", + "jwt" + ] + }, + "type": "array", + "uniqueItems": true, + "title": "Response Modes", + "description": "The Response Modes this client is authorized request." + }, + "authorization_policy": { + "type": "string", + "title": "Authorization Policy", + "description": "The Authorization Policy to apply to this client." + }, + "lifespan": { + "type": "string", + "title": "Lifespan Name", + "description": "The name of the custom lifespan to utilize for this client." + }, + "requested_audience_mode": { + "type": "string", + "enum": [ + "explicit", + "implicit" + ], + "title": "Requested Audience Mode", + "description": "The Requested Audience Mode used for this client." + }, + "consent_mode": { + "type": "string", + "enum": [ + "auto", + "explicit", + "implicit", + "pre-configured" + ], + "title": "Consent Mode", + "description": "The Consent Mode used for this client." + }, + "pre_configured_consent_duration": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Pre-Configured Consent Duration", + "description": "The Pre-Configured Consent Duration when using Consent Mode pre-configured for this client." + }, + "require_pushed_authorization_requests": { + "type": "boolean", + "title": "Require Pushed Authorization Requests", + "description": "Requires Pushed Authorization Requests for this client to perform an authorization.", + "default": false + }, + "require_pkce": { + "type": "boolean", + "title": "Require PKCE", + "description": "Requires a Proof Key for this client to perform Code Exchange.", + "default": false + }, + "pkce_challenge_method": { + "type": "string", + "enum": [ + "plain", + "S256" + ], + "title": "PKCE Challenge Method", + "description": "The PKCE Challenge Method enforced on this client." + }, + "authorization_signed_response_alg": { + "type": "string", + "enum": [ + "none", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Authorization Response Signing Algorithm", + "description": "The Authorization Endpoint Signing Algorithm this client uses.", + "default": "none" + }, + "authorization_signed_response_key_id": { + "type": "string", + "title": "Authorization Response Signing Key ID", + "description": "The Key ID this client uses to sign the Authorization responses (overrides the 'authorization_signed_response_alg')." + }, + "id_token_signed_response_alg": { + "type": "string", + "enum": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "ID Token Signing Algorithm", + "description": "The algorithm (JWA) this client uses to sign ID Tokens.", + "default": "RS256" + }, + "id_token_signed_response_key_id": { + "type": "string", + "title": "ID Token Signing Key ID", + "description": "The Key ID this client uses to sign ID Tokens (overrides the 'id_token_signing_alg')." + }, + "access_token_signed_response_alg": { + "type": "string", + "enum": [ + "none", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Access Token Signing Algorithm", + "description": "The algorithm (JWA) this client uses to sign Access Tokens.", + "default": "none" + }, + "access_token_signed_response_key_id": { + "type": "string", + "title": "Access Token Signing Key ID", + "description": "The Key ID this client uses to sign Access Tokens (overrides the 'access_token_signed_response_alg')." + }, + "userinfo_signed_response_alg": { + "type": "string", + "enum": [ + "none", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "UserInfo Response Signing Algorithm", + "description": "The UserInfo Endpoint Signing Algorithm this client uses.", + "default": "none" + }, + "userinfo_signed_response_key_id": { + "type": "string", + "title": "UserInfo Response Signing Key ID", + "description": "The Key ID this client uses to sign the UserInfo responses (overrides the 'userinfo_signed_response_alg')." + }, + "introspection_signed_response_alg": { + "type": "string", + "enum": [ + "none", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Introspection Response Signing Algorithm", + "description": "The Introspection Endpoint Signing Algorithm this client uses.", + "default": "none" + }, + "introspection_signed_response_key_id": { + "type": "string", + "title": "Introspection Response Signing Key ID", + "description": "The Key ID this client uses to sign the Introspection responses (overrides the 'introspection_signed_response_alg')." + }, + "request_object_signing_alg": { + "type": "string", + "enum": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Request Object Signing Algorithm", + "description": "The Request Object Signing Algorithm the provider accepts for this client." + }, + "token_endpoint_auth_signing_alg": { + "type": "string", + "enum": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Token Endpoint Auth Signing Algorithm", + "description": "The Token Endpoint Auth Signing Algorithm the provider accepts for this client." + }, + "token_endpoint_auth_method": { + "type": "string", + "enum": [ + "none", + "client_secret_post", + "client_secret_basic", + "private_key_jwt", + "client_secret_jwt" + ], + "title": "Token Endpoint Auth Method", + "description": "The Token Endpoint Auth Method enforced by the provider for this client." + }, + "allow_multiple_auth_methods": { + "type": "boolean", + "title": "Allow Multiple Authentication Methods", + "description": "Permits this registered client to accept misbehaving clients which use a broad authentication approach. This is not standards complaint, use at your own security risk." + }, + "jwks_uri": { + "type": "string", + "format": "uri", + "title": "JSON Web Keys URI", + "description": "URI of the JWKS endpoint which contains the Public Keys used to validate request objects and the 'private_key_jwt' client authentication method for this client." + }, + "jwks": { + "items": { + "$ref": "#/$defs/JWK" + }, + "type": "array", + "title": "JSON Web Keys", + "description": "List of arbitrary Public Keys used to validate request objects and the 'private_key_jwt' client authentication method for this client." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "client_id", + "scopes" + ], + "description": "IdentityProvidersOpenIDConnectClient represents a configuration for an OpenID Connect 1.0 client." + }, + "IdentityProvidersOpenIDConnectClientURIs": { + "oneOf": [ + { + "type": "string", + "format": "uri" + }, + { + "items": { + "type": "string", + "format": "uri" + }, + "type": "array", + "uniqueItems": true + } + ] + }, + "IdentityProvidersOpenIDConnectLifespan": { + "properties": { + "access_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Access Token Lifespan", + "description": "The duration an Access Token is valid for." + }, + "authorize_code": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Authorize Code Lifespan", + "description": "The duration an Authorization Code is valid for." + }, + "id_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "ID Token Lifespan", + "description": "The duration an ID Token is valid for." + }, + "refresh_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Refresh Token Lifespan", + "description": "The duration a Refresh Token is valid for." + }, + "grants": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespanGrants", + "title": "Grant Types", + "description": "Allows tuning the token lifespans for individual grant types." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnectLifespan allows tuning the lifespans for OpenID Connect 1.0 issued tokens." + }, + "IdentityProvidersOpenIDConnectLifespanGrants": { + "properties": { + "authorize_code": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespanToken", + "title": "Authorize Code Grant", + "description": "Allows tuning the token lifespans for the authorize code grant." + }, + "implicit": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespanToken", + "title": "Implicit Grant", + "description": "Allows tuning the token lifespans for the implicit flow and grant." + }, + "client_credentials": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespanToken", + "title": "Client Credentials Grant", + "description": "Allows tuning the token lifespans for the client credentials grant." + }, + "refresh_token": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespanToken", + "title": "Refresh Token Grant", + "description": "Allows tuning the token lifespans for the refresh token grant." + }, + "jwt_bearer": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespanToken", + "title": "JWT Bearer Grant", + "description": "Allows tuning the token lifespans for the JWT bearer grant." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnectLifespanGrants allows tuning the lifespans for each grant type." + }, + "IdentityProvidersOpenIDConnectLifespanToken": { + "properties": { + "access_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Access Token Lifespan", + "description": "The duration an Access Token is valid for." + }, + "authorize_code": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Authorize Code Lifespan", + "description": "The duration an Authorization Code is valid for." + }, + "id_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "ID Token Lifespan", + "description": "The duration an ID Token is valid for." + }, + "refresh_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Refresh Token Lifespan", + "description": "The duration a Refresh Token is valid for." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnectLifespanToken allows tuning the lifespans for each token type." + }, + "IdentityProvidersOpenIDConnectLifespans": { + "properties": { + "access_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Access Token Lifespan", + "description": "The duration an Access Token is valid for." + }, + "authorize_code": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Authorize Code Lifespan", + "description": "The duration an Authorization Code is valid for." + }, + "id_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "ID Token Lifespan", + "description": "The duration an ID Token is valid for." + }, + "refresh_token": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Refresh Token Lifespan", + "description": "The duration a Refresh Token is valid for." + }, + "jwt_secured_authorization": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "JARM", + "description": "Allows tuning the token lifespan for the JWT Secured Authorization Response Mode (JARM)." + }, + "custom": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectLifespan" + } + }, + "type": "object", + "title": "Custom Lifespans", + "description": "Allows creating custom lifespans to be used by individual clients." + } + }, + "additionalProperties": false, + "type": "object" + }, + "IdentityProvidersOpenIDConnectPolicy": { + "properties": { + "default_policy": { + "type": "string", + "enum": [ + "one_factor", + "two_factor", + "deny" + ], + "title": "Default Policy", + "description": "The default policy action for this policy." + }, + "rules": { + "items": { + "$ref": "#/$defs/IdentityProvidersOpenIDConnectPolicyRule" + }, + "type": "array", + "title": "Rules", + "description": "The list of rules for this policy." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnectPolicy configuration for OpenID Connect 1.0 authorization policies." + }, + "IdentityProvidersOpenIDConnectPolicyRule": { + "properties": { + "policy": { + "type": "string", + "enum": [ + "one_factor", + "two_factor", + "deny" + ], + "title": "Policy", + "description": "The policy to apply to this rule." + }, + "subject": { + "$ref": "#/$defs/AccessControlRuleSubjects", + "title": "Subject", + "description": "Allows tuning the token lifespans for the authorize code grant." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityProvidersOpenIDConnectPolicyRule configuration for OpenID Connect 1.0 authorization policies rules." + }, + "IdentityValidation": { + "properties": { + "reset_password": { + "$ref": "#/$defs/IdentityValidationResetPassword", + "title": "Reset Password", + "description": "Identity Validation options for the Reset Password flow." + }, + "elevated_session": { + "$ref": "#/$defs/IdentityValidationElevatedSession", + "title": "Elevated Session", + "description": "Identity Validation options for obtaining an Elevated Session for flows such as the Credential Management flows." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityValidation represents the configuration for identity verification actions/flows." + }, + "IdentityValidationElevatedSession": { + "properties": { + "code_lifespan": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Code Lifespan", + "description": "The lifespan of the randomly generated One Time Code after which it's considered invalid." + }, + "elevation_lifespan": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Elevation Lifespan", + "description": "The lifespan of the elevation after initially validating the One-Time Code before it expires." + }, + "otp_characters": { + "type": "integer", + "maximum": 12, + "minimum": 6, + "title": "OTP Characters", + "description": "Number of characters in the generated OTP codes.", + "default": 8 + }, + "require_second_factor": { + "type": "boolean", + "title": "Require Second Factor", + "description": "Requires the user use a second factor if they have any known second factor methods.", + "default": false + }, + "skip_second_factor": { + "type": "boolean", + "title": "Skip Second Factor", + "description": "Skips the primary identity verification process if the user has authenticated with a second factor.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityValidationElevatedSession represents the tunable aspects of the credential control identity verification action/flow." + }, + "IdentityValidationResetPassword": { + "properties": { + "jwt_lifespan": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "JWT Lifespan", + "description": "The lifespan of the JSON Web Token after it's initially generated after which it's considered invalid." + }, + "jwt_algorithm": { + "type": "string", + "enum": [ + "HS256", + "HS384", + "HS512" + ], + "title": "JWT Algorithm", + "description": "The JSON Web Token Algorithm (JWA) used to sign the Reset Password flow JSON Web Token's.", + "default": "HS256" + }, + "jwt_secret": { + "type": "string", + "title": "JWT Secret", + "description": "The secret key used to sign the Reset Password flow JSON Web Token's." + } + }, + "additionalProperties": false, + "type": "object", + "description": "IdentityValidationResetPassword represents the tunable aspects of the reset password identity verification action/flow." + }, + "JWK": { + "properties": { + "key_id": { + "type": "string", + "maxLength": 100, + "title": "Key ID", + "description": "The ID of this JWK." + }, + "use": { + "type": "string", + "enum": [ + "sig" + ], + "title": "Use", + "description": "The Use of this JWK.", + "default": "sig" + }, + "algorithm": { + "type": "string", + "enum": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "title": "Algorithm", + "description": "The Algorithm of this JWK." + }, + "key": { + "type": "string", + "pattern": "^-{5}BEGIN (((RSA|EC) )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-{5}\\n([a-zA-Z0-9\\/+]{1,64}\\n)+([a-zA-Z0-9\\/+]{1,64}[=]{0,2})\\n-{5}END (((RSA|EC) )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-{5}\\n?$", + "description": "The Private/Public key material of this JWK in Base64 PEM format." + }, + "certificate_chain": { + "$ref": "#/$defs/X509CertificateChain", + "title": "Certificate Chain", + "description": "The optional associated certificate which matches the Key public key portion for this JWK." + } + }, + "additionalProperties": false, + "type": "object", + "description": "JWK represents a JWK." + }, + "Log": { + "properties": { + "level": { + "type": "string", + "enum": [ + "error", + "warn", + "info", + "debug", + "trace" + ], + "title": "Level", + "description": "The minimum Level a Log message must be before it's added to the log." + }, + "format": { + "type": "string", + "enum": [ + "json", + "text" + ], + "title": "Format", + "description": "The Format of Log messages." + }, + "file_path": { + "type": "string", + "title": "File Path", + "description": "The File Path to save the logs to instead of sending them to stdout, it's strongly recommended this option is only enabled with 'keep_stdout' also enabled." + }, + "keep_stdout": { + "type": "boolean", + "title": "Keep Stdout", + "description": "Enables keeping stdout when using the File Path option.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "Log represents the logging configuration." + }, + "NTP": { + "properties": { + "address": { + "$ref": "#/$defs/AddressUDP", + "title": "NTP Address", + "description": "The remote address of the NTP server." + }, + "version": { + "type": "integer", + "enum": [ + 3, + 4 + ], + "title": "NTP Version", + "description": "The NTP Version to use." + }, + "max_desync": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Maximum Desync", + "description": "The maximum amount of time that the server can be out of sync." + }, + "disable_startup_check": { + "type": "boolean", + "title": "Disable Startup Check", + "description": "Disables the NTP Startup Check entirely.", + "default": false + }, + "disable_failure": { + "type": "boolean", + "title": "Disable Failure", + "description": "Disables complete failure whe the Startup Check fails and instead just logs the error.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "NTP represents the configuration related to ntp server." + }, + "Notifier": { + "properties": { + "disable_startup_check": { + "type": "boolean", + "title": "Disable Startup Check", + "description": "Disables the notifier startup checks.", + "default": false + }, + "filesystem": { + "$ref": "#/$defs/NotifierFileSystem", + "title": "File System", + "description": "The File System notifier." + }, + "smtp": { + "$ref": "#/$defs/NotifierSMTP", + "title": "SMTP", + "description": "The SMTP notifier." + }, + "template_path": { + "type": "string", + "title": "Template Path", + "description": "The path for notifier template overrides." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Notifier represents the configuration of the notifier to use when sending notifications to users." + }, + "NotifierFileSystem": { + "properties": { + "filename": { + "type": "string", + "title": "Filename", + "description": "The file path of the notifications." + } + }, + "additionalProperties": false, + "type": "object", + "description": "NotifierFileSystem represents the configuration of the notifier writing emails in a file." + }, + "NotifierSMTP": { + "properties": { + "address": { + "$ref": "#/$defs/AddressSMTP", + "title": "Address", + "description": "The SMTP server address." + }, + "timeout": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Timeout", + "description": "The SMTP server connection timeout." + }, + "username": { + "type": "string", + "title": "Username", + "description": "The username for SMTP authentication." + }, + "password": { + "type": "string", + "title": "Password", + "description": "The password for SMTP authentication." + }, + "identifier": { + "type": "string", + "title": "Identifier", + "description": "The identifier used during the HELO/EHLO command.", + "default": "localhost" + }, + "sender": { + "oneOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "string", + "pattern": "^[^\u003c]+\\s\\\u003c[a-zA-Z0-9._~!#$%\u0026'*/=?^{|}+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z0-9-]+\\\u003e$" + } + ], + "title": "Sender", + "description": "The sender used for SMTP." + }, + "subject": { + "type": "string", + "title": "Subject", + "description": "The subject format used.", + "default": "[Authelia] {title}" + }, + "startup_check_address": { + "oneOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "string", + "pattern": "^[^\u003c]+\\s\\\u003c[a-zA-Z0-9._~!#$%\u0026'*/=?^{|}+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z0-9-]+\\\u003e$" + } + ], + "title": "Startup Check Address", + "description": "The address used for the recipient in the startup check." + }, + "disable_require_tls": { + "type": "boolean", + "title": "Disable Require TLS", + "description": "Disables the requirement to use TLS.", + "default": false + }, + "disable_html_emails": { + "type": "boolean", + "title": "Disable HTML Emails", + "description": "Disables the mixed content type of emails and only sends the plaintext version.", + "default": false + }, + "disable_starttls": { + "type": "boolean", + "title": "Disable StartTLS", + "description": "Disables the opportunistic StartTLS functionality which is useful for bad SMTP servers which advertise support for it but don't actually support it.", + "default": false + }, + "tls": { + "$ref": "#/$defs/TLS", + "title": "TLS", + "description": "The SMTP server TLS connection properties." + }, + "host": { + "type": "string", + "description": "Deprecated: use address instead.", + "deprecated": true + }, + "port": { + "type": "integer", + "description": "Deprecated: use address instead.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "NotifierSMTP represents the configuration of the SMTP server to send emails with." + }, + "PasswordDigest": { + "type": "string", + "pattern": "^\\$((argon2(id|i|d)\\$v=19\\$m=\\d+,t=\\d+,p=\\d+|scrypt\\$ln=\\d+,r=\\d+,p=\\d+)\\$[a-zA-Z0-9\\/+]+\\$[a-zA-Z0-9\\/+]+|pbkdf2(-sha(224|256|384|512))?\\$\\d+\\$[a-zA-Z0-9\\/.]+\\$[a-zA-Z0-9\\/.]+|bcrypt-sha256\\$v=2,t=2b,r=\\d+\\$[a-zA-Z0-9\\/.]+\\$[a-zA-Z0-9\\/.]+|2(a|b|y)?\\$\\d+\\$[a-zA-Z0-9.\\/]+|(5|6)\\$rounds=\\d+\\$[a-zA-Z0-9.\\/]+\\$[a-zA-Z0-9.\\/]+|plaintext\\$.+|base64\\$[a-zA-Z0-9.=\\/]+)$" + }, + "PasswordPolicy": { + "properties": { + "standard": { + "$ref": "#/$defs/PasswordPolicyStandard", + "title": "Standard", + "description": "The standard password policy engine." + }, + "zxcvbn": { + "$ref": "#/$defs/PasswordPolicyZXCVBN", + "title": "ZXCVBN", + "description": "The ZXCVBN password policy engine." + } + }, + "additionalProperties": false, + "type": "object", + "description": "PasswordPolicy represents the configuration related to password policy." + }, + "PasswordPolicyStandard": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enables the standard password policy engine.", + "default": false + }, + "min_length": { + "type": "integer", + "title": "Minimum Length", + "description": "Minimum password length." + }, + "max_length": { + "type": "integer", + "title": "Maximum Length", + "description": "Maximum password length.", + "default": 8 + }, + "require_uppercase": { + "type": "boolean", + "title": "Require Uppercase", + "description": "Require uppercase characters.", + "default": false + }, + "require_lowercase": { + "type": "boolean", + "title": "Require Lowercase", + "description": "Require lowercase characters.", + "default": false + }, + "require_number": { + "type": "boolean", + "title": "Require Number", + "description": "Require numeric characters.", + "default": false + }, + "require_special": { + "type": "boolean", + "title": "Require Special", + "description": "Require symbolic characters.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "PasswordPolicyStandard represents the configuration related to standard parameters of password policy." + }, + "PasswordPolicyZXCVBN": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enables the ZXCVBN password policy engine.", + "default": false + }, + "min_score": { + "type": "integer", + "title": "Minimum Score", + "description": "The minimum ZXCVBN score allowed.", + "default": 3 + } + }, + "additionalProperties": false, + "type": "object", + "description": "PasswordPolicyZXCVBN represents the configuration related to ZXCVBN parameters of password policy." + }, + "PrivacyPolicy": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enables the Privacy Policy functionality.", + "default": false + }, + "require_user_acceptance": { + "type": "boolean", + "title": "Require User Acceptance", + "description": "Enables the requirement for users to accept the policy.", + "default": false + }, + "policy_url": { + "type": "string", + "format": "uri", + "title": "Policy URL", + "description": "The URL of the privacy policy." + } + }, + "additionalProperties": false, + "type": "object", + "description": "PrivacyPolicy is the privacy policy configuration." + }, + "RefreshIntervalDuration": { + "oneOf": [ + { + "type": "string", + "enum": [ + "always", + "never" + ] + }, + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "default": "5 minutes" + }, + "Regulation": { + "properties": { + "max_retries": { + "type": "integer", + "title": "Maximum Retries", + "description": "The maximum number of failed attempts permitted before banning a user.", + "default": 3 + }, + "find_time": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Find Time", + "description": "The amount of time to consider when determining the number of failed attempts." + }, + "ban_time": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Ban Time", + "description": "The amount of time to ban the user for when it's determined the maximum retries has been exceeded." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Regulation represents the configuration related to regulation." + }, + "Server": { + "properties": { + "address": { + "$ref": "#/$defs/AddressTCP", + "title": "Address", + "description": "The address to listen on." + }, + "asset_path": { + "type": "string", + "title": "Asset Path", + "description": "The directory where the server asset overrides reside." + }, + "disable_healthcheck": { + "type": "boolean", + "title": "Disable Healthcheck", + "description": "Disables the healthcheck functionality.", + "default": false + }, + "tls": { + "$ref": "#/$defs/ServerTLS", + "title": "TLS", + "description": "The server TLS configuration." + }, + "headers": { + "$ref": "#/$defs/ServerHeaders", + "title": "Headers", + "description": "The server headers configuration." + }, + "endpoints": { + "$ref": "#/$defs/ServerEndpoints", + "title": "Endpoints", + "description": "The server endpoints configuration." + }, + "buffers": { + "$ref": "#/$defs/ServerBuffers", + "title": "Buffers", + "description": "The server buffers configuration." + }, + "timeouts": { + "$ref": "#/$defs/ServerTimeouts", + "title": "Timeouts", + "description": "The server timeouts configuration." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Server represents the configuration of the http server." + }, + "ServerBuffers": { + "properties": { + "read": { + "type": "integer", + "title": "Read", + "description": "The read buffer size.", + "default": 4096 + }, + "write": { + "type": "integer", + "title": "Write", + "description": "The write buffer size.", + "default": 4096 + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerBuffers represents server buffer configurations." + }, + "ServerEndpoints": { + "properties": { + "enable_pprof": { + "type": "boolean", + "title": "Enable PProf", + "description": "Enables the developer specific pprof endpoints which should not be used in production and only used for debugging purposes.", + "default": false + }, + "enable_expvars": { + "type": "boolean", + "title": "Enable ExpVars", + "description": "Enables the developer specific ExpVars endpoints which should not be used in production and only used for debugging purposes.", + "default": false + }, + "authz": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/ServerEndpointsAuthz" + } + }, + "type": "object", + "title": "Authz", + "description": "Configures the Authorization endpoints." + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerEndpoints is the endpoints configuration for the HTTP server." + }, + "ServerEndpointsAuthz": { + "properties": { + "implementation": { + "type": "string", + "enum": [ + "ForwardAuth", + "AuthRequest", + "ExtAuthz", + "Legacy" + ], + "title": "Implementation", + "description": "The specific Authorization implementation to use for this endpoint." + }, + "authn_strategies": { + "items": { + "$ref": "#/$defs/ServerEndpointsAuthzAuthnStrategy" + }, + "type": "array", + "title": "Authn Strategies", + "description": "The specific Authorization strategies to use for this endpoint." + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerEndpointsAuthz is the Authz endpoints configuration for the HTTP server." + }, + "ServerEndpointsAuthzAuthnStrategy": { + "properties": { + "name": { + "type": "string", + "enum": [ + "HeaderAuthorization", + "HeaderProxyAuthorization", + "HeaderAuthRequestProxyAuthorization", + "HeaderLegacy", + "CookieSession" + ], + "title": "Name", + "description": "The name of the Authorization strategy to use." + }, + "schemes": { + "items": { + "type": "string", + "enum": [ + "basic", + "bearer" + ] + }, + "type": "array", + "title": "Authorization Schemes", + "description": "The name of the authorization schemes to allow with the header strategies.", + "default": [ + "basic" + ] + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerEndpointsAuthzAuthnStrategy is the Authz endpoints configuration for the HTTP server." + }, + "ServerHeaders": { + "properties": { + "csp_template": { + "type": "string", + "title": "CSP Template", + "description": "The Content Security Policy template.", + "default": "default-src 'self'; frame-src 'none'; object-src 'none'; style-src 'self' 'nonce-%s'; frame-ancestors 'none'; base-uri 'self'" + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerHeaders represents the customization of the http server headers." + }, + "ServerTLS": { + "properties": { + "certificate": { + "type": "string", + "title": "Certificate", + "description": "Path to the Certificate." + }, + "key": { + "type": "string", + "title": "Key", + "description": "Path to the Private Key." + }, + "client_certificates": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true, + "title": "Client Certificates", + "description": "Path to the Client Certificates to trust for mTLS." + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerTLS represents the configuration of the http servers TLS options." + }, + "ServerTimeouts": { + "properties": { + "read": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Read", + "description": "The read timeout." + }, + "write": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Write", + "description": "The write timeout." + }, + "idle": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Idle", + "description": "The idle timeout." + } + }, + "additionalProperties": false, + "type": "object", + "description": "ServerTimeouts represents server timeout configurations." + }, + "Session": { + "properties": { + "name": { + "type": "string", + "description": "The session cookie name.", + "default": "authelia_session" + }, + "same_site": { + "type": "string", + "enum": [ + "lax", + "strict", + "none" + ], + "description": "The session cookie same site value.", + "default": "lax" + }, + "expiration": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "description": "The session cookie expiration when remember me is not checked." + }, + "inactivity": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "description": "The session inactivity timeout." + }, + "remember_me": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "description": "The session cookie expiration when remember me is checked." + }, + "secret": { + "type": "string", + "title": "Secret", + "description": "Secret used to encrypt the session data." + }, + "cookies": { + "items": { + "$ref": "#/$defs/SessionCookie" + }, + "type": "array", + "title": "Cookies", + "description": "List of cookie domain configurations." + }, + "redis": { + "$ref": "#/$defs/SessionRedis", + "title": "Redis", + "description": "Redis Session Provider configuration." + }, + "domain": { + "type": "string", + "title": "Domain", + "description": "Deprecated: Use the session cookies option with the same name instead.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "Session represents the configuration related to user sessions." + }, + "SessionCookie": { + "properties": { + "name": { + "type": "string", + "description": "The session cookie name.", + "default": "authelia_session" + }, + "same_site": { + "type": "string", + "enum": [ + "lax", + "strict", + "none" + ], + "description": "The session cookie same site value.", + "default": "lax" + }, + "expiration": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "description": "The session cookie expiration when remember me is not checked." + }, + "inactivity": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "description": "The session inactivity timeout." + }, + "remember_me": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "description": "The session cookie expiration when remember me is checked." + }, + "domain": { + "type": "string", + "format": "hostname", + "title": "Domain", + "description": "The domain for this session cookie configuration." + }, + "authelia_url": { + "type": "string", + "format": "uri", + "title": "Authelia URL", + "description": "The Root Authelia URL to redirect users to for this session cookie configuration." + }, + "default_redirection_url": { + "type": "string", + "format": "uri", + "title": "Default Redirection URL", + "description": "The default redirection URL for this session cookie configuration." + } + }, + "additionalProperties": false, + "type": "object", + "description": "SessionCookie represents the configuration for a cookie domain." + }, + "SessionRedis": { + "properties": { + "host": { + "type": "string", + "title": "Host", + "description": "The redis server host." + }, + "port": { + "type": "integer", + "title": "Host", + "description": "The redis server port.", + "default": 6379 + }, + "timeout": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Timeout", + "description": "The Redis server connection timeout." + }, + "max_retries": { + "type": "integer", + "title": "Maximum Retries", + "description": "The maximum number of retries on a failed command.", + "default": 3 + }, + "username": { + "type": "string", + "title": "Username", + "description": "The redis username." + }, + "password": { + "type": "string", + "title": "Password", + "description": "The redis password." + }, + "database_index": { + "type": "integer", + "title": "Database Index", + "description": "The redis database index.", + "default": 0 + }, + "maximum_active_connections": { + "type": "integer", + "title": "Maximum Active Connections", + "description": "The maximum connections that can be made to redis at one time.", + "default": 8 + }, + "minimum_idle_connections": { + "type": "integer", + "title": "Minimum Idle Connections", + "description": "The minimum idle connections that should be open to redis." + }, + "tls": { + "$ref": "#/$defs/TLS" + }, + "high_availability": { + "$ref": "#/$defs/SessionRedisHighAvailability" + } + }, + "additionalProperties": false, + "type": "object", + "description": "SessionRedis represents the configuration related to redis session store." + }, + "SessionRedisHighAvailability": { + "properties": { + "sentinel_name": { + "type": "string", + "title": "Sentinel Name", + "description": "The name of the sentinel instance." + }, + "sentinel_username": { + "type": "string", + "title": "Sentinel Username", + "description": "The username for the sentinel instance." + }, + "sentinel_password": { + "type": "string", + "title": "Sentinel Username", + "description": "The username for the sentinel instance." + }, + "route_by_latency": { + "type": "boolean", + "title": "Route by Latency", + "description": "Uses the Route by Latency mode.", + "default": false + }, + "route_randomly": { + "type": "boolean", + "title": "Route Randomly", + "description": "Uses the Route Randomly mode.", + "default": false + }, + "nodes": { + "items": { + "$ref": "#/$defs/SessionRedisHighAvailabilityNode" + }, + "type": "array", + "title": "Nodes", + "description": "The pre-populated list of nodes for the sentinel instance." + } + }, + "additionalProperties": false, + "type": "object", + "description": "SessionRedisHighAvailability holds configuration variables for Redis Cluster/Sentinel." + }, + "SessionRedisHighAvailabilityNode": { + "properties": { + "host": { + "type": "string", + "title": "Host", + "description": "The redis sentinel node host." + }, + "port": { + "type": "integer", + "title": "Port", + "description": "The redis sentinel node port.", + "default": 26379 + } + }, + "additionalProperties": false, + "type": "object", + "description": "SessionRedisHighAvailabilityNode Represents a Node." + }, + "Storage": { + "properties": { + "local": { + "$ref": "#/$defs/StorageLocal", + "title": "Local", + "description": "The Local SQLite3 Storage configuration settings." + }, + "mysql": { + "$ref": "#/$defs/StorageMySQL", + "title": "MySQL", + "description": "The MySQL/MariaDB Storage configuration settings." + }, + "postgres": { + "$ref": "#/$defs/StoragePostgreSQL", + "title": "PostgreSQL", + "description": "The PostgreSQL Storage configuration settings." + }, + "encryption_key": { + "type": "string", + "title": "Encryption Key", + "description": "The Storage Encryption Key used to secure security sensitive values in the storage engine." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Storage represents the configuration of the storage backend." + }, + "StorageLocal": { + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "The Path for the SQLite3 database file." + } + }, + "additionalProperties": false, + "type": "object", + "description": "StorageLocal represents the configuration when using local storage." + }, + "StorageMySQL": { + "properties": { + "address": { + "$ref": "#/$defs/AddressTCP", + "title": "Address", + "description": "The address of the database." + }, + "database": { + "type": "string", + "title": "Database", + "description": "The database name to use upon a successful connection." + }, + "username": { + "type": "string", + "title": "Username", + "description": "The username to use to authenticate." + }, + "password": { + "type": "string", + "title": "Password", + "description": "The password to use to authenticate." + }, + "timeout": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Timeout", + "description": "The timeout for the database connection." + }, + "tls": { + "$ref": "#/$defs/TLS" + } + }, + "additionalProperties": false, + "type": "object", + "description": "StorageMySQL represents the configuration of a MySQL database." + }, + "StoragePostgreSQL": { + "properties": { + "address": { + "$ref": "#/$defs/AddressTCP", + "title": "Address", + "description": "The address of the database." + }, + "database": { + "type": "string", + "title": "Database", + "description": "The database name to use upon a successful connection." + }, + "username": { + "type": "string", + "title": "Username", + "description": "The username to use to authenticate." + }, + "password": { + "type": "string", + "title": "Password", + "description": "The password to use to authenticate." + }, + "timeout": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Timeout", + "description": "The timeout for the database connection." + }, + "schema": { + "type": "string", + "title": "Schema", + "description": "The default schema name to use.", + "default": "public" + }, + "tls": { + "$ref": "#/$defs/TLS" + }, + "ssl": { + "$ref": "#/$defs/StoragePostgreSQLSSL", + "title": "SSL", + "description": "Deprecated: Use the TLS configuration instead.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "StoragePostgreSQL represents the configuration of a PostgreSQL database." + }, + "StoragePostgreSQLSSL": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "disable", + "verify-ca", + "require", + "verify-full" + ], + "title": "Mode", + "description": "The SSL mode to use, deprecated and replaced with the TLS options.", + "deprecated": true + }, + "root_certificate": { + "type": "string", + "title": "Root Certificate", + "description": "Path to the Root Certificate to use, deprecated and replaced with the TLS options.", + "deprecated": true + }, + "certificate": { + "type": "string", + "title": "Certificate", + "description": "Path to the Certificate to use, deprecated and replaced with the TLS options.", + "deprecated": true + }, + "key": { + "type": "string", + "title": "Key", + "description": "Path to the Private Key to use, deprecated and replaced with the TLS options.", + "deprecated": true + } + }, + "additionalProperties": false, + "type": "object", + "description": "StoragePostgreSQLSSL represents the SSL configuration of a PostgreSQL database." + }, + "TLS": { + "properties": { + "minimum_version": { + "$ref": "#/$defs/TLSVersion", + "title": "Minimum Version", + "description": "The minimum TLS version accepted." + }, + "maximum_version": { + "$ref": "#/$defs/TLSVersion", + "title": "Maximum Version", + "description": "The maximum TLS version accepted." + }, + "skip_verify": { + "type": "boolean", + "title": "Skip Verify", + "description": "Disable all verification of the TLS properties.", + "default": false + }, + "server_name": { + "type": "string", + "format": "hostname", + "title": "Server Name", + "description": "The expected server name to match the certificate against." + }, + "private_key": { + "type": "string", + "pattern": "^-{5}BEGIN ((RSA|EC) )?PRIVATE KEY-{5}\\n([a-zA-Z0-9\\/+]{1,64}\\n)+([a-zA-Z0-9\\/+]{1,64}[=]{0,2})\\n-{5}END ((RSA|EC) )?PRIVATE KEY-{5}\\n?$", + "title": "Private Key", + "description": "The private key." + }, + "certificate_chain": { + "$ref": "#/$defs/X509CertificateChain", + "title": "Certificate Chain", + "description": "The certificate chain." + } + }, + "additionalProperties": false, + "type": "object", + "description": "TLS is a representation of the TLS configuration." + }, + "TLSVersion": { + "type": "string", + "enum": [ + "TLS1.0", + "TLS1.1", + "TLS1.2", + "TLS1.3" + ] + }, + "TOTP": { + "properties": { + "disable": { + "type": "boolean", + "title": "Disable", + "description": "Disables the TOTP 2FA functionality.", + "default": false + }, + "issuer": { + "type": "string", + "title": "Issuer", + "description": "The issuer value for generated TOTP keys.", + "default": "Authelia" + }, + "algorithm": { + "type": "string", + "enum": [ + "SHA1", + "SHA256", + "SHA512" + ], + "title": "Algorithm", + "description": "The algorithm value for generated TOTP keys.", + "default": "SHA1" + }, + "digits": { + "type": "integer", + "enum": [ + 6, + 8 + ], + "title": "Digits", + "description": "The digits value for generated TOTP keys.", + "default": 6 + }, + "period": { + "type": "integer", + "title": "Period", + "description": "The period value for generated TOTP keys.", + "default": 30 + }, + "skew": { + "type": "integer", + "title": "Skew", + "description": "The permitted skew for generated TOTP keys.", + "default": 1 + }, + "secret_size": { + "type": "integer", + "minimum": 20, + "title": "Secret Size", + "description": "The secret size for generated TOTP keys.", + "default": 32 + }, + "allowed_algorithms": { + "items": { + "type": "string", + "enum": [ + "SHA1", + "SHA256", + "SHA512" + ] + }, + "type": "array", + "title": "Allowed Algorithms", + "description": "List of algorithms the user is allowed to select in addition to the default.", + "default": [ + "SHA1" + ] + }, + "allowed_digits": { + "items": { + "type": "integer", + "enum": [ + 6, + 8 + ] + }, + "type": "array", + "title": "Allowed Digits", + "description": "List of digits the user is allowed to select in addition to the default.", + "default": [ + 6 + ] + }, + "allowed_periods": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Allowed Periods", + "description": "List of periods the user is allowed to select in addition to the default.", + "default": [ + 30 + ] + }, + "disable_reuse_security_policy": { + "type": "boolean", + "title": "Disable Reuse Security Policy", + "description": "Disables the security policy that prevents reuse of a TOTP code.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "description": "TOTP represents the configuration related to TOTP options." + }, + "Telemetry": { + "properties": { + "metrics": { + "$ref": "#/$defs/TelemetryMetrics", + "title": "Metrics", + "description": "The telemetry metrics server configuration." + } + }, + "additionalProperties": false, + "type": "object", + "description": "Telemetry represents the telemetry config." + }, + "TelemetryMetrics": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enables the metrics server.", + "default": false + }, + "address": { + "$ref": "#/$defs/AddressTCP", + "title": "Address", + "description": "The address for the metrics server to listen on." + }, + "buffers": { + "$ref": "#/$defs/ServerBuffers", + "title": "Buffers", + "description": "The server buffers configuration for the metrics server." + }, + "timeouts": { + "$ref": "#/$defs/ServerTimeouts", + "title": "Timeouts", + "description": "The server timeouts configuration for the metrics server." + } + }, + "additionalProperties": false, + "type": "object", + "description": "TelemetryMetrics represents the telemetry metrics config." + }, + "WebAuthn": { + "properties": { + "disable": { + "type": "boolean", + "title": "Disable", + "description": "Disables the WebAuthn 2FA functionality.", + "default": false + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "The display name attribute for the WebAuthn relying party.", + "default": "Authelia" + }, + "attestation_conveyance_preference": { + "type": "string", + "enum": [ + "none", + "indirect", + "direct" + ], + "title": "Conveyance Preference", + "description": "The default conveyance preference for all WebAuthn credentials.", + "default": "indirect" + }, + "user_verification": { + "type": "string", + "enum": [ + "discouraged", + "preferred", + "required" + ], + "title": "User Verification", + "description": "The default user verification preference for all WebAuthn credentials.", + "default": "preferred" + }, + "timeout": { + "oneOf": [ + { + "type": "string", + "pattern": "^\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?))(\\s*\\d+\\s*(y|M|w|d|h|m|s|ms|((year|month|week|day|hour|minute|second|millisecond)s?)))*$" + }, + { + "type": "integer", + "description": "The duration in seconds" + } + ], + "title": "Timeout", + "description": "The default timeout for all WebAuthn ceremonies." + } + }, + "additionalProperties": false, + "type": "object", + "description": "WebAuthn represents the webauthn config." + }, + "X509CertificateChain": { + "type": "string", + "pattern": "^(-{5}BEGIN CERTIFICATE-{5}\\n([a-zA-Z0-9\\/+]{1,64}\\n)+([a-zA-Z0-9\\/+]{1,64}[=]{0,2})\\n-{5}END CERTIFICATE-{5}\\n?)+$" + } + } +}
\ No newline at end of file diff --git a/docs/static/schemas/v4.39/json-schema/exports.identifiers.json b/docs/static/schemas/v4.39/json-schema/exports.identifiers.json new file mode 100644 index 000000000..8c434245f --- /dev/null +++ b/docs/static/schemas/v4.39/json-schema/exports.identifiers.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.authelia.com/schemas/v4.39/json-schema/exports.identifiers.json", + "$ref": "#/$defs/UserOpaqueIdentifiersExport", + "$defs": { + "UUID": { + "items": { + "type": "integer" + }, + "type": "array", + "maxItems": 16, + "minItems": 16 + }, + "UserOpaqueIdentifier": { + "properties": { + "ID": { + "type": "integer" + }, + "service": { + "type": "string", + "title": "Service", + "description": "The service name this UUID is used with." + }, + "sector_id": { + "type": "string", + "title": "Sector Identifier", + "description": "Sector Identifier this UUID is used with." + }, + "username": { + "type": "string", + "title": "Username", + "description": "The username of the user this UUID is for." + }, + "identifier": { + "$ref": "#/$defs/UUID", + "title": "Identifier", + "description": "The random UUID for this opaque identifier." + } + }, + "additionalProperties": false, + "type": "object" + }, + "UserOpaqueIdentifiersExport": { + "properties": { + "identifiers": { + "items": { + "$ref": "#/$defs/UserOpaqueIdentifier" + }, + "type": "array", + "title": "Identifiers", + "description": "The list of opaque identifiers." + } + }, + "additionalProperties": false, + "type": "object" + } + } +}
\ No newline at end of file diff --git a/docs/static/schemas/v4.39/json-schema/exports.totp.json b/docs/static/schemas/v4.39/json-schema/exports.totp.json new file mode 100644 index 000000000..f7c111073 --- /dev/null +++ b/docs/static/schemas/v4.39/json-schema/exports.totp.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.authelia.com/schemas/v4.39/json-schema/exports.totp.json", + "$ref": "#/$defs/TOTPConfigurationDataExport", + "$defs": { + "TOTPConfigurationData": { + "properties": { + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The time the configuration was created." + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "title": "Last Used At", + "description": "The time the configuration was last used at." + }, + "username": { + "type": "string", + "title": "Username", + "description": "The username of the user this configuration belongs to." + }, + "issuer": { + "type": "string", + "title": "Issuer", + "description": "The issuer name this was generated with." + }, + "algorithm": { + "type": "string", + "title": "Algorithm", + "description": "The algorithm this configuration uses." + }, + "digits": { + "type": "integer", + "title": "Digits", + "description": "The number of digits this configuration uses." + }, + "period": { + "type": "integer", + "title": "Period", + "description": "The period of time this configuration uses." + }, + "secret": { + "type": "string", + "title": "Secret", + "description": "The secret shared key for this configuration." + } + }, + "additionalProperties": false, + "type": "object" + }, + "TOTPConfigurationDataExport": { + "properties": { + "totp_configurations": { + "items": { + "$ref": "#/$defs/TOTPConfigurationData" + }, + "type": "array", + "title": "TOTP Configurations", + "description": "The list of TOTP configurations." + } + }, + "additionalProperties": false, + "type": "object" + } + } +}
\ No newline at end of file diff --git a/docs/static/schemas/v4.39/json-schema/exports.webauthn.json b/docs/static/schemas/v4.39/json-schema/exports.webauthn.json new file mode 100644 index 000000000..e2f233eab --- /dev/null +++ b/docs/static/schemas/v4.39/json-schema/exports.webauthn.json @@ -0,0 +1,130 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.authelia.com/schemas/v4.39/json-schema/exports.webauthn.json", + "$ref": "#/$defs/WebAuthnCredentialDataExport", + "$defs": { + "WebAuthnCredentialData": { + "properties": { + "id": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The time this credential was created." + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "title": "Last Used At", + "description": "The last time this credential was used." + }, + "rpid": { + "type": "string", + "title": "Relying Party ID", + "description": "The Relying Party ID used to register this credential." + }, + "username": { + "type": "string", + "title": "Username", + "description": "The username of the user this credential belongs to." + }, + "description": { + "type": "string", + "title": "Description", + "description": "The user description of this credential." + }, + "kid": { + "type": "string", + "title": "Public Key ID", + "description": "The Public Key ID of this credential." + }, + "aaguid": { + "type": "string", + "title": "AAGUID", + "description": "The Authenticator Attestation Global Unique Identifier of this credential." + }, + "attestation_type": { + "type": "string", + "title": "Attestation Type", + "description": "The attestation format type this credential uses." + }, + "attachment": { + "type": "string", + "title": "Attachment", + "description": "The last recorded credential attachment type." + }, + "transports": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Transports", + "description": "The last recorded credential transports." + }, + "sign_count": { + "type": "integer", + "title": "Sign Count", + "description": "The last recorded credential sign count." + }, + "clone_warning": { + "type": "boolean", + "title": "Clone Warning", + "description": "The clone warning status of the credential." + }, + "legacy": { + "type": "boolean", + "title": "Legacy", + "description": "The legacy value indicates this credential may need to be registered again." + }, + "discoverable": { + "type": "boolean", + "title": "Discoverable", + "description": "The discoverable status of this credential." + }, + "present": { + "type": "boolean", + "title": "Present", + "description": "The user presence status of this credential." + }, + "verified": { + "type": "boolean", + "title": "Verified", + "description": "The verified status of this credential." + }, + "backup_eligible": { + "type": "boolean", + "title": "Backup Eligible", + "description": "The backup eligible status of this credential." + }, + "backup_state": { + "type": "boolean", + "title": "Backup Eligible", + "description": "The backup eligible status of this credential." + }, + "public_key": { + "type": "string", + "title": "Public Key", + "description": "The credential public key." + } + }, + "additionalProperties": false, + "type": "object" + }, + "WebAuthnCredentialDataExport": { + "properties": { + "webauthn_credentials": { + "items": { + "$ref": "#/$defs/WebAuthnCredentialData" + }, + "type": "array", + "title": "WebAuthn Credentials", + "description": "The list of WebAuthn credentials." + } + }, + "additionalProperties": false, + "type": "object" + } + } +}
\ No newline at end of file diff --git a/docs/static/schemas/v4.39/json-schema/user-database.json b/docs/static/schemas/v4.39/json-schema/user-database.json new file mode 100644 index 000000000..2ee85aab0 --- /dev/null +++ b/docs/static/schemas/v4.39/json-schema/user-database.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.authelia.com/schemas/v4.39/json-schema/user-database.json", + "$ref": "#/$defs/FileUserDatabase", + "$defs": { + "FileUserDatabase": { + "properties": { + "users": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/FileUserDatabaseUserDetails" + } + }, + "type": "object", + "title": "Users", + "description": "The dictionary of users." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "users" + ], + "description": "FileUserDatabase is a user details database that is concurrency safe database and can be reloaded." + }, + "FileUserDatabaseUserDetails": { + "properties": { + "password": { + "$ref": "#/$defs/PasswordDigest", + "title": "Password", + "description": "The hashed password for the user." + }, + "displayname": { + "type": "string", + "title": "Display Name", + "description": "The display name for the user." + }, + "email": { + "type": "string", + "title": "Email", + "description": "The email for the user." + }, + "groups": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Groups", + "description": "The groups list for the user." + }, + "disabled": { + "type": "boolean", + "title": "Disabled", + "description": "The disabled status for the user.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "password", + "displayname" + ], + "description": "FileUserDatabaseUserDetails is the model of user details in the file database." + }, + "PasswordDigest": { + "type": "string", + "pattern": "^\\$((argon2(id|i|d)\\$v=19\\$m=\\d+,t=\\d+,p=\\d+|scrypt\\$ln=\\d+,r=\\d+,p=\\d+)\\$[a-zA-Z0-9\\/+]+\\$[a-zA-Z0-9\\/+]+|pbkdf2(-sha(224|256|384|512))?\\$\\d+\\$[a-zA-Z0-9\\/.]+\\$[a-zA-Z0-9\\/.]+|bcrypt-sha256\\$v=2,t=2b,r=\\d+\\$[a-zA-Z0-9\\/.]+\\$[a-zA-Z0-9\\/.]+|2(a|b|y)?\\$\\d+\\$[a-zA-Z0-9.\\/]+|(5|6)\\$rounds=\\d+\\$[a-zA-Z0-9.\\/]+\\$[a-zA-Z0-9.\\/]+|plaintext\\$.+|base64\\$[a-zA-Z0-9.=\\/]+)$" + } + } +}
\ No newline at end of file diff --git a/internal/authorization/access_control_rule.go b/internal/authorization/access_control_rule.go index fa2953ca6..9c862b425 100644 --- a/internal/authorization/access_control_rule.go +++ b/internal/authorization/access_control_rule.go @@ -1,30 +1,26 @@ package authorization import ( - "net" - "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/utils" ) // NewAccessControlRules converts a schema.AccessControl into an AccessControlRule slice. func NewAccessControlRules(config schema.AccessControl) (rules []*AccessControlRule) { - networksMap, networksCacheMap := parseSchemaNetworks(config.Networks) - for i, schemaRule := range config.Rules { - rules = append(rules, NewAccessControlRule(i+1, schemaRule, networksMap, networksCacheMap)) + rules = append(rules, NewAccessControlRule(i+1, schemaRule)) } return rules } // NewAccessControlRule parses a schema ACL and generates an internal ACL. -func NewAccessControlRule(pos int, rule schema.AccessControlRule, networksMap map[string][]*net.IPNet, networksCacheMap map[string]*net.IPNet) *AccessControlRule { +func NewAccessControlRule(pos int, rule schema.AccessControlRule) *AccessControlRule { r := &AccessControlRule{ Position: pos, Query: NewAccessControlQuery(rule.Query), Methods: schemaMethodsToACL(rule.Methods), - Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap), + Networks: AccessControlNetworks(rule.Networks), Subjects: schemaSubjectsToACL(rule.Subjects), Policy: NewLevel(rule.Policy), } @@ -49,7 +45,7 @@ type AccessControlRule struct { Resources []AccessControlResource Query []AccessControlQuery Methods []string - Networks []*net.IPNet + Networks AccessControlNetworks Subjects []AccessControlSubjects Policy Level } @@ -146,19 +142,7 @@ func (acr *AccessControlRule) MatchesMethods(object Object) (match bool) { // 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 len(acr.Networks) == 0 { - return true - } - - // Iterate over the networks until we find a match (return true) or until we exit the loop (return false). - for _, network := range acr.Networks { - if network.Contains(subject.IP) { - return true - } - } - - return false + return acr.Networks.IsMatch(subject) } // MatchesSubjects returns true if the rule matches the subjects. diff --git a/internal/authorization/authorizer_test.go b/internal/authorization/authorizer_test.go index 679b4e78f..d6bffe1bd 100644 --- a/internal/authorization/authorizer_test.go +++ b/internal/authorization/authorizer_test.go @@ -12,6 +12,7 @@ import ( "github.com/valyala/fasthttp" "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/utils" ) type AuthorizerSuite struct { @@ -680,32 +681,46 @@ func (s *AuthorizerSuite) TestShouldCheckMultipleSubjectsMatching() { } func (s *AuthorizerSuite) TestShouldCheckIPMatching() { + must := func(in []string) []*net.IPNet { + out := make([]*net.IPNet, len(in)) + + var err error + + for i := range in { + if out[i], err = utils.ParseHostCIDR(in[i]); err != nil { + panic(err) + } + } + + return out + } + tester := NewAuthorizerBuilder(). WithDefaultPolicy(deny). WithRule(schema.AccessControlRule{ Domains: []string{"protected.example.com"}, Policy: bypass, - Networks: []string{"192.168.1.8", "10.0.0.8"}, + Networks: must([]string{"192.168.1.8", "10.0.0.8"}), }). WithRule(schema.AccessControlRule{ Domains: []string{"protected.example.com"}, Policy: oneFactor, - Networks: []string{"10.0.0.7"}, + Networks: must([]string{"10.0.0.7"}), }). WithRule(schema.AccessControlRule{ Domains: []string{"net.example.com"}, Policy: twoFactor, - Networks: []string{"10.0.0.0/8"}, + Networks: must([]string{"10.0.0.0/8"}), }). WithRule(schema.AccessControlRule{ Domains: []string{"ipv6.example.com"}, Policy: twoFactor, - Networks: []string{"fec0::1/64"}, + Networks: must([]string{"fec0::1/64"}), }). WithRule(schema.AccessControlRule{ Domains: []string{"ipv6-alt.example.com"}, Policy: twoFactor, - Networks: []string{"fec0::1"}, + Networks: must([]string{"fec0::1"}), }). Build() diff --git a/internal/authorization/types.go b/internal/authorization/types.go index ec517ccff..9f3f1d918 100644 --- a/internal/authorization/types.go +++ b/internal/authorization/types.go @@ -9,6 +9,22 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) +type AccessControlNetworks []*net.IPNet + +func (a AccessControlNetworks) IsMatch(subject Subject) bool { + if len(a) == 0 { + return true + } + + for _, network := range a { + if network.Contains(subject.IP) { + return true + } + } + + return false +} + // SubjectMatcher is a matcher that takes a subject. type SubjectMatcher interface { IsMatch(subject Subject) (match bool) diff --git a/internal/authorization/util.go b/internal/authorization/util.go index 7d3621b88..cccdd2454 100644 --- a/internal/authorization/util.go +++ b/internal/authorization/util.go @@ -1,7 +1,6 @@ package authorization import ( - "net" "regexp" "strings" @@ -121,74 +120,6 @@ func schemaMethodsToACL(methodRules []string) (methods []string) { return methods } -func schemaNetworksToACL(networkRules []string, networksMap map[string][]*net.IPNet, networksCacheMap map[string]*net.IPNet) (networks []*net.IPNet) { - for _, network := range networkRules { - if _, ok := networksMap[network]; !ok { - if _, ok := networksCacheMap[network]; ok { - networks = append(networks, networksCacheMap[network]) - } else { - cidr, err := parseNetwork(network) - if err == nil { - networks = append(networks, cidr) - networksCacheMap[cidr.String()] = cidr - - if cidr.String() != network { - networksCacheMap[network] = cidr - } - } - } - } else { - networks = append(networks, networksMap[network]...) - } - } - - return networks -} - -func parseSchemaNetworks(schemaNetworks []schema.AccessControlNetwork) (networksMap map[string][]*net.IPNet, networksCacheMap map[string]*net.IPNet) { - // These maps store pointers to the net.IPNet values so we can reuse them efficiently. - // The networksMap contains the named networks as keys, the networksCacheMap contains the CIDR notations as keys. - networksMap = map[string][]*net.IPNet{} - networksCacheMap = map[string]*net.IPNet{} - - for _, aclNetwork := range schemaNetworks { - var networks []*net.IPNet - - for _, networkRule := range aclNetwork.Networks { - cidr, err := parseNetwork(networkRule) - if err == nil { - networks = append(networks, cidr) - networksCacheMap[cidr.String()] = cidr - - if cidr.String() != networkRule { - networksCacheMap[networkRule] = cidr - } - } - } - - if _, ok := networksMap[aclNetwork.Name]; len(networks) != 0 && !ok { - networksMap[aclNetwork.Name] = networks - } - } - - return networksMap, networksCacheMap -} - -func parseNetwork(networkRule string) (cidr *net.IPNet, err error) { - if !strings.Contains(networkRule, "/") { - ip := net.ParseIP(networkRule) - if ip.To4() != nil { - _, cidr, err = net.ParseCIDR(networkRule + "/32") - } else { - _, cidr, err = net.ParseCIDR(networkRule + "/128") - } - } else { - _, cidr, err = net.ParseCIDR(networkRule) - } - - return cidr, err -} - func schemaSubjectsToACL(subjectRules [][]string) (subjects []AccessControlSubjects) { for _, subjectRule := range subjectRules { subject := AccessControlSubjects{} diff --git a/internal/authorization/util_test.go b/internal/authorization/util_test.go index 811509b30..8b241084f 100644 --- a/internal/authorization/util_test.go +++ b/internal/authorization/util_test.go @@ -59,151 +59,6 @@ func TestShouldSplitDomainCorrectly(t *testing.T) { assert.Equal(t, "com", suffix) } -func TestShouldParseRuleNetworks(t *testing.T) { - schemaNetworks := []schema.AccessControlNetwork{ - { - Name: "desktop", - Networks: []string{ - "10.0.0.1", - }, - }, - { - Name: "lan", - Networks: []string{ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - }, - }, - } - - _, firstNetwork, err := net.ParseCIDR("192.168.1.20/32") - require.NoError(t, err) - - networksMap, networksCacheMap := parseSchemaNetworks(schemaNetworks) - - assert.Len(t, networksCacheMap, 5) - - networks := []string{"192.168.1.20", "lan"} - - acl := schemaNetworksToACL(networks, networksMap, networksCacheMap) - - assert.Len(t, networksCacheMap, 7) - - require.Len(t, acl, 4) - assert.Equal(t, firstNetwork, acl[0]) - assert.Equal(t, networksMap["lan"][0], acl[1]) - assert.Equal(t, networksMap["lan"][1], acl[2]) - assert.Equal(t, networksMap["lan"][2], acl[3]) - - // Check they are the same memory address. - assert.True(t, networksMap["lan"][0] == acl[1]) - assert.True(t, networksMap["lan"][1] == acl[2]) - assert.True(t, networksMap["lan"][2] == acl[3]) - - assert.False(t, firstNetwork == acl[0]) -} - -func TestShouldParseACLNetworks(t *testing.T) { - schemaNetworks := []schema.AccessControlNetwork{ - { - Name: "test", - Networks: []string{ - "10.0.0.1", - }, - }, - { - Name: "second", - Networks: []string{ - "10.0.0.1", - }, - }, - { - Name: "duplicate", - Networks: []string{ - "10.0.0.1", - }, - }, - { - Name: "duplicate", - Networks: []string{ - "10.0.0.1", - }, - }, - { - Name: "ipv6", - Networks: []string{ - "fec0::1", - }, - }, - { - Name: "ipv6net", - Networks: []string{ - "fec0::1/64", - }, - }, - { - Name: "net", - Networks: []string{ - "10.0.0.0/8", - }, - }, - { - Name: "badnet", - Networks: []string{ - "bad/8", - }, - }, - } - - _, firstNetwork, err := net.ParseCIDR("10.0.0.1/32") - require.NoError(t, err) - - _, secondNetwork, err := net.ParseCIDR("10.0.0.0/8") - require.NoError(t, err) - - _, thirdNetwork, err := net.ParseCIDR("fec0::1/64") - require.NoError(t, err) - - _, fourthNetwork, err := net.ParseCIDR("fec0::1/128") - require.NoError(t, err) - - networksMap, networksCacheMap := parseSchemaNetworks(schemaNetworks) - - require.Len(t, networksMap, 6) - require.Contains(t, networksMap, "test") - require.Contains(t, networksMap, "second") - require.Contains(t, networksMap, "duplicate") - require.Contains(t, networksMap, "ipv6") - require.Contains(t, networksMap, "ipv6net") - require.Contains(t, networksMap, "net") - require.Len(t, networksMap["test"], 1) - - require.Len(t, networksCacheMap, 7) - require.Contains(t, networksCacheMap, "10.0.0.1") - require.Contains(t, networksCacheMap, "10.0.0.1/32") - require.Contains(t, networksCacheMap, "10.0.0.1/32") - require.Contains(t, networksCacheMap, "10.0.0.0/8") - require.Contains(t, networksCacheMap, "fec0::1") - require.Contains(t, networksCacheMap, "fec0::1/128") - require.Contains(t, networksCacheMap, "fec0::1/64") - - assert.Equal(t, firstNetwork, networksMap["test"][0]) - assert.Equal(t, secondNetwork, networksMap["net"][0]) - assert.Equal(t, thirdNetwork, networksMap["ipv6net"][0]) - assert.Equal(t, fourthNetwork, networksMap["ipv6"][0]) - - assert.Equal(t, firstNetwork, networksCacheMap["10.0.0.1"]) - assert.Equal(t, firstNetwork, networksCacheMap["10.0.0.1/32"]) - - assert.Equal(t, secondNetwork, networksCacheMap["10.0.0.0/8"]) - - assert.Equal(t, thirdNetwork, networksCacheMap["fec0::1/64"]) - - assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1"]) - assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"]) -} - func TestIsAuthLevelSufficient(t *testing.T) { assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Denied)) assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, Denied)) @@ -249,40 +104,6 @@ func TestStringSliceToRegexpSlice(t *testing.T) { } } -func TestSchemaNetworksToACL(t *testing.T) { - testCases := []struct { - name string - have []string - globals map[string][]*net.IPNet - cache map[string]*net.IPNet - expected []*net.IPNet - }{ - { - "ShouldLoadFromCache", - []string{"192.168.0.0/24"}, - nil, - map[string]*net.IPNet{"192.168.0.0/24": MustParseCIDR("192.168.0.0/24")}, - []*net.IPNet{MustParseCIDR("192.168.0.0/24")}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.globals == nil { - tc.globals = map[string][]*net.IPNet{} - } - - if tc.cache == nil { - tc.cache = map[string]*net.IPNet{} - } - - actual := schemaNetworksToACL(tc.have, tc.globals, tc.cache) - - assert.Equal(t, tc.expected, actual) - }) - } -} - func TestIsOpenIDConnectMFA(t *testing.T) { testCases := []struct { name string diff --git a/internal/commands/context.go b/internal/commands/context.go index ec1c58d67..c36a17996 100644 --- a/internal/commands/context.go +++ b/internal/commands/context.go @@ -397,7 +397,8 @@ func (ctx *CmdCtx) ConfigEnsureExistsRunE(cmd *cobra.Command, _ []string) (err e // HelperConfigLoadRunE loads the configuration into the CmdCtx. func (ctx *CmdCtx) HelperConfigLoadRunE(cmd *cobra.Command, _ []string) (err error) { var ( - filters []configuration.BytesFilter + definitions *schema.Definitions + filters []configuration.BytesFilter ) if ctx.cconfig == nil { @@ -426,10 +427,15 @@ func (ctx *CmdCtx) HelperConfigLoadRunE(cmd *cobra.Command, _ []string) (err err ctx.cconfig.defaults, ctx.cconfig.sources...) + if definitions, err = configuration.LoadDefinitions(ctx.cconfig.validator, ctx.cconfig.sources...); err != nil { + return err + } + if ctx.cconfig.keys, err = configuration.LoadAdvanced( ctx.cconfig.validator, "", ctx.config, + definitions, ctx.cconfig.sources...); err != nil { return err } diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index c6231783d..7ee0e16eb 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -323,6 +323,22 @@ identity_validation: # disable_failure: false ## +## Definitions +## +## The definitions are used in other areas as reference points to reduce duplication. +## +# definitions: + ## The network definitions. + # network: + ## The name of the definition followed by the list of CIDR network addresses in this definition. + # internal: + # - '10.10.0.0/16' + # - '172.16.0.0/12' + # - '192.168.2.0/24' + # VPN: + # - '10.9.0.0/16' + +## ## Authentication Backend Provider Configuration ## ## Used for verifying user passwords and retrieve information such as email address and groups users belong to. @@ -622,14 +638,6 @@ identity_validation: ## resource if there is no policy to be applied to the user. # 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 applied to everyone # - domain: 'public.example.com' diff --git a/internal/configuration/decode_hooks.go b/internal/configuration/decode_hooks.go index 02610567e..c92834d2f 100644 --- a/internal/configuration/decode_hooks.go +++ b/internal/configuration/decode_hooks.go @@ -5,7 +5,9 @@ import ( "crypto/ed25519" "crypto/rsa" "crypto/x509" + "encoding/base64" "fmt" + "net" "net/mail" "net/url" "reflect" @@ -602,6 +604,14 @@ func StringToCryptographicKeyHookFunc() mapstructure.DecodeHookFuncType { dataStr := data.(string) if value, err = utils.ParseX509FromPEM([]byte(dataStr)); err != nil { + if !strings.Contains(dataStr, "\n") && !strings.HasPrefix(dataStr, "-----") { + var key []byte + + if key, err = base64.URLEncoding.DecodeString(dataStr); err == nil { + return key, nil + } + } + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "", expectedType, err) } @@ -750,3 +760,66 @@ func StringToPasswordDigestHookFunc() mapstructure.DecodeHookFuncType { return *result, nil } } + +//nolint:gocyclo +func StringToIPNetworksHookFunc(definitions map[string][]*net.IPNet) mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data any) (value any, err error) { + if f.Kind() != reflect.String && (f.Kind() != reflect.Slice || (f.Elem().Kind() != reflect.Interface && f.Elem().Kind() != reflect.String)) { + return data, nil + } + + expectedType := reflect.TypeOf(net.IPNet{}) + + isSlice := t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Ptr && t.Elem().Elem() == expectedType + isKind := t.Kind() == reflect.Ptr && t.Elem() == expectedType + + if !isSlice && !isKind { + return data, nil + } + + var values []string + + switch d := data.(type) { + case string: + values = []string{d} + case []string: + values = d + case []any: + values = make([]string, 0, len(d)) + + for i := range d { + switch v := d[i].(type) { + case string: + values = append(values, v) + default: + values = append(values, fmt.Sprint(v)) + } + } + } + + var ( + ok bool + definition []*net.IPNet + networks []*net.IPNet + network *net.IPNet + ) + + for _, str := range values { + if definitions != nil { + if definition, ok = definitions[str]; ok { + networks = append(networks, definition...) + + continue + } + } + + if network, err = utils.ParseHostCIDR(str); err != nil { + return nil, fmt.Errorf("failed to parse network %q: %w", str, err) + } + + networks = append(networks, network) + } + + return networks, nil + } +} diff --git a/internal/configuration/decode_hooks_test.go b/internal/configuration/decode_hooks_test.go index 6612c7511..85c09dd84 100644 --- a/internal/configuration/decode_hooks_test.go +++ b/internal/configuration/decode_hooks_test.go @@ -10,9 +10,11 @@ import ( "encoding/pem" "fmt" "math" + "net" "net/mail" "net/url" "os" + "path" "reflect" "regexp" "strings" @@ -2007,6 +2009,93 @@ func TestStringToX509CertificateChainHookFunc(t *testing.T) { } } +func TestStringToIPNetworksHookFunc(t *testing.T) { + mustParseNet := func(in string) *net.IPNet { + _, n, err := net.ParseCIDR(in) + if err != nil { + panic(err) + } + + return n + } + + testCases := []struct { + name string + path string + have *schema.Definitions + expected TestConfigDefinitions + err string + }{ + { + "ShouldDecode", + "decode_networks.yml", + &schema.Definitions{}, + TestConfigDefinitions{ + Definitions: schema.Definitions{ + Network: map[string][]*net.IPNet{ + "single": { + mustParseNet("192.168.0.1/32"), + }, + "example": { + mustParseNet("192.168.1.20/32"), + mustParseNet("192.168.2.0/24"), + }, + }, + }, + }, + "", + }, + { + "ShouldDecodeDefinitions", + "decode_networks_abc.yml", + &schema.Definitions{ + Network: map[string][]*net.IPNet{ + "abc": { + mustParseNet("1.1.1.1/32"), + mustParseNet("1.1.1.2/32"), + }, + }, + }, + TestConfigDefinitions{ + Definitions: schema.Definitions{ + Network: map[string][]*net.IPNet{ + "example": { + mustParseNet("192.168.1.20/32"), + mustParseNet("192.168.2.0/24"), + mustParseNet("1.1.1.1/32"), + mustParseNet("1.1.1.2/32"), + }, + }, + }, + }, + "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := TestConfigDefinitions{} + + val := schema.NewStructValidator() + _, err := configuration.LoadAdvanced(val, "", &result, tc.have, configuration.NewDefaultSourcesFiltered([]string{path.Join("./test_resources", tc.path)}, nil, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)...) + + if tc.err == "" { + assert.NoError(t, err) + + assert.Equal(t, tc.expected, result) + + assert.Len(t, val.Errors(), 0) + } else { + assert.EqualError(t, err, tc.err) + } + }) + } +} + +type TestConfigDefinitions struct { + Definitions schema.Definitions `koanf:"definitions"` +} + var ( //nolint:gosec x509PrivateKeyRSABad = ` diff --git a/internal/configuration/koanf_provider_filtered_file.go b/internal/configuration/koanf_provider_filtered_file.go index 0e54773b7..0b44e389c 100644 --- a/internal/configuration/koanf_provider_filtered_file.go +++ b/internal/configuration/koanf_provider_filtered_file.go @@ -18,6 +18,7 @@ import ( // FilteredFile implements a koanf.Provider. type FilteredFile struct { + data []byte path string filters []BytesFilter } @@ -32,21 +33,25 @@ func FilteredFileProvider(path string, filters ...BytesFilter) *FilteredFile { // ReadBytes reads the contents of a file on disk, passes it through any configured filters, and returns the bytes. func (f *FilteredFile) ReadBytes() (data []byte, err error) { - if data, err = os.ReadFile(f.path); err != nil { + if f.data != nil { + return f.data, nil + } + + if f.data, err = os.ReadFile(f.path); err != nil { return nil, err } - if len(data) == 0 || len(f.filters) == 0 { - return data, nil + if len(f.data) == 0 || len(f.filters) == 0 { + return f.data, nil } for _, filter := range f.filters { - if data, err = filter.Filter(data); err != nil { + if f.data, err = filter.Filter(f.data); err != nil { return nil, err } } - return data, nil + return f.data, nil } // Read is not supported by the filtered file koanf.Provider. @@ -54,6 +59,7 @@ func (f *FilteredFile) Read() (map[string]any, error) { return nil, errors.New("filtered file provider does not support this method") } +// BytesFilter is an interface describing utility structures that filter bytes into desired bytes. type BytesFilter interface { Name() (name string) Filter(in []byte) (out []byte, err error) diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index c4f68260d..77c0b3577 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -2,6 +2,7 @@ package configuration import ( "fmt" + "net" "github.com/go-viper/mapstructure/v2" "github.com/knadh/koanf/v2" @@ -13,21 +14,18 @@ import ( func Load(val *schema.StructValidator, sources ...Source) (keys []string, configuration *schema.Configuration, err error) { configuration = &schema.Configuration{} - keys, err = LoadAdvanced(val, "", configuration, sources...) + keys, err = LoadAdvanced(val, "", configuration, nil, sources...) return keys, configuration, err } // LoadAdvanced is intended to give more flexibility over loading a particular path to a specific interface. -func LoadAdvanced(val *schema.StructValidator, path string, result any, sources ...Source) (keys []string, err error) { +func LoadAdvanced(val *schema.StructValidator, path string, result any, definitions *schema.Definitions, sources ...Source) (keys []string, err error) { if val == nil { return keys, errNoValidator } - ko := koanf.NewWithConf(koanf.Conf{ - Delim: constDelimiter, - StrictMerge: false, - }) + ko := koanf.NewWithConf(koanf.Conf{Delim: constDelimiter, StrictMerge: false}) if err = loadSources(ko, val, sources...); err != nil { return ko.Keys(), err @@ -39,11 +37,88 @@ func LoadAdvanced(val *schema.StructValidator, path string, result any, sources return koanfGetKeys(ko), err } - unmarshal(final, val, path, result) + unmarshal(final, val, path, result, definitions) + + mapDefinitionsResult(val, result) return koanfGetKeys(final), nil } +func LoadDefinitions(val *schema.StructValidator, sources ...Source) (definitions *schema.Definitions, err error) { + ko := koanf.NewWithConf(koanf.Conf{Delim: constDelimiter, StrictMerge: false}) + + if err = loadSources(ko, val, sources...); err != nil { + return nil, err + } + + var final *koanf.Koanf + + if final, err = koanfRemapKeys(val, ko, deprecations, deprecationsMKM); err != nil { + return nil, err + } + + legacy := &legacyDefinitions{} + + c := koanf.UnmarshalConf{ + DecoderConfig: &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToSliceHookFunc(","), + StringToIPNetworksHookFunc(nil), + ), + Metadata: nil, + Result: legacy, + WeaklyTypedInput: true, + }, + } + + if err = final.UnmarshalWithConf("", legacy, c); err != nil { + val.Push(fmt.Errorf("error occurred during unmarshalling definitions configuration: %w", err)) + } + + d := legacy.Definitions + + mapDefinitions(val, legacy.AccessControl.Networks, &d) + + return &d, nil +} + +func mapDefinitionsResult(val *schema.StructValidator, result any) { + if config, ok := result.(*schema.Configuration); ok { + mapDefinitions(val, config.AccessControl.Networks, &config.Definitions) + } +} + +func mapDefinitions(val *schema.StructValidator, networks []schema.AccessControlNetwork, definitions *schema.Definitions) { + if len(networks) == 0 { + return + } + + var ok bool + + if definitions.Network == nil { + definitions.Network = make(map[string][]*net.IPNet, len(networks)) + } + + for _, network := range networks { + if _, ok = definitions.Network[network.Name]; ok { + val.Push(fmt.Errorf("error occurred during unmarshalling definitions configuration: the definition for network with name '%s' exists in both the definitions section and access control section which is not permitted", network.Name)) + + continue + } + + definitions.Network[network.Name] = network.Networks + } +} + +type legacyDefinitions struct { + Definitions schema.Definitions `koanf:"definitions"` + AccessControl legacyAccessControl `koanf:"access_control"` +} + +type legacyAccessControl struct { + Networks []schema.AccessControlNetwork `koanf:"networks"` +} + func mapHasKey(k string, m map[string]any) bool { if _, ok := m[k]; ok { return true @@ -52,7 +127,11 @@ func mapHasKey(k string, m map[string]any) bool { return false } -func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o any) { +func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o any, definitions *schema.Definitions) { + if definitions == nil { + definitions = &schema.Definitions{} + } + c := koanf.UnmarshalConf{ DecoderConfig: &mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc( @@ -68,6 +147,7 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o any) StringToCryptographicKeyHookFunc(), StringToTLSVersionHookFunc(), StringToPasswordDigestHookFunc(), + StringToIPNetworksHookFunc(definitions.Network), ToTimeDurationHookFunc(), ToRefreshIntervalDurationHookFunc(), ), diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 390ac4532..40e399769 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -1165,6 +1165,48 @@ func TestShouldFailIfYmlIsInvalid(t *testing.T) { assert.ErrorContains(t, val.Errors()[0], "unmarshal errors") } +func TestConfigurationDefinitions(t *testing.T) { + var ( + definitions *schema.Definitions + err error + ) + + val := schema.NewStructValidator() + + config := &schema.Configuration{} + + sources := NewDefaultSourcesWithDefaults([]string{"./test_resources/config_with_definitions.yml"}, nil, DefaultEnvPrefix, DefaultEnvDelimiter, nil) + + definitions, err = LoadDefinitions(val, sources...) + + require.NoError(t, err) + + _, err = LoadAdvanced(val, "", config, definitions, sources...) + + require.NoError(t, err) + + require.Len(t, config.Definitions.Network, 2) + + require.Contains(t, config.Definitions.Network, "lan") + require.Len(t, config.Definitions.Network["lan"], 2) + assert.Equal(t, "192.168.1.0/24", config.Definitions.Network["lan"][0].String()) + assert.Equal(t, "192.168.2.0/24", config.Definitions.Network["lan"][1].String()) + + require.Contains(t, config.Definitions.Network, "abc") + require.Len(t, config.Definitions.Network["abc"], 2) + assert.Equal(t, "192.168.3.0/24", config.Definitions.Network["abc"][0].String()) + assert.Equal(t, "192.168.4.0/24", config.Definitions.Network["abc"][1].String()) + + require.Len(t, config.AccessControl.Rules, 12) + require.Len(t, config.AccessControl.Rules[1].Networks, 5) + + assert.Equal(t, "192.168.0.0/24", config.AccessControl.Rules[1].Networks[0].String()) + assert.Equal(t, "192.168.1.0/24", config.AccessControl.Rules[1].Networks[1].String()) + assert.Equal(t, "192.168.2.0/24", config.AccessControl.Rules[1].Networks[2].String()) + assert.Equal(t, "192.168.3.0/24", config.AccessControl.Rules[1].Networks[3].String()) + assert.Equal(t, "192.168.4.0/24", config.AccessControl.Rules[1].Networks[4].String()) +} + func TestConfigurationTemplate(t *testing.T) { buf := &bytes.Buffer{} @@ -1242,7 +1284,21 @@ func TestConfigurationTemplate(t *testing.T) { val := schema.NewStructValidator() - keys, _, err := Load(val, NewBytesSource(config)) + var ( + keys []string + definitions *schema.Definitions + ) + + c := &schema.Configuration{} + + src := NewBytesSource(config) + + definitions, err = LoadDefinitions(val, src) + + require.NoError(t, err) + + keys, err = LoadAdvanced(val, "", c, definitions, src) + require.NoError(t, err) assert.Len(t, val.Errors(), 0) diff --git a/internal/configuration/schema/access_control.go b/internal/configuration/schema/access_control.go index 5671042a4..c5f969520 100644 --- a/internal/configuration/schema/access_control.go +++ b/internal/configuration/schema/access_control.go @@ -1,5 +1,7 @@ package schema +import "net" + // AccessControl represents the configuration related to ACLs. type AccessControl struct { // The default policy if no other policy matches the request. @@ -14,8 +16,8 @@ type AccessControl struct { // AccessControlNetwork represents one ACL network group entry. type AccessControlNetwork struct { - Name string `koanf:"name" json:"name" jsonschema:"required,title=Network Name" jsonschema_description:"The name of this network to be used in the networks section of the rules section."` - Networks AccessControlNetworkNetworks `koanf:"networks" json:"networks" jsonschema:"required,title=Networks" jsonschema_description:"The remote IP's or network ranges in CIDR notation that this rule applies to."` + Name string `koanf:"name" json:"name" jsonschema:"required,title=Network Name" jsonschema_description:"The name of this network to be used in the networks section of the rules section."` + Networks []*net.IPNet `koanf:"networks" json:"networks" jsonschema:"required,title=Networks" jsonschema_description:"The remote IP's or network ranges in CIDR notation that this rule applies to."` } // AccessControlRule represents one ACL rule entry. @@ -24,7 +26,7 @@ type AccessControlRule struct { DomainsRegex AccessControlRuleRegex `koanf:"domain_regex" json:"domain_regex" jsonschema:"oneof_required=Domain Regex,title=Domain Regex Patterns" jsonschema_description:"The regex patterns to match the domain against that this rule applies to."` Policy string `koanf:"policy" json:"policy" jsonschema:"required,enum=bypass,enum=deny,enum=one_factor,enum=two_factor,title=Rule Policy" jsonschema_description:"The policy this rule applies when all criteria match."` Subjects AccessControlRuleSubjects `koanf:"subject" json:"subject" jsonschema:"title=AccessControlRuleSubjects" jsonschema_description:"The users or groups that this rule applies to."` - Networks AccessControlRuleNetworks `koanf:"networks" json:"networks" jsonschema:"title=Networks" jsonschema_description:"The remote IP's, network ranges in CIDR notation, or network names that this rule applies to."` + Networks []*net.IPNet `koanf:"networks" json:"networks" jsonschema:"title=Networks" jsonschema_description:"The remote IP's, network ranges in CIDR notation, or network definition names that this rule applies to."` Resources AccessControlRuleRegex `koanf:"resources" json:"resources" jsonschema:"title=Resources or Paths" jsonschema_description:"The regex patterns to match the resource paths that this rule applies to."` Methods AccessControlRuleMethods `koanf:"methods" json:"methods" jsonschema:"enum=GET,enum=HEAD,enum=POST,enum=PUT,enum=DELETE,enum=CONNECT,enum=OPTIONS,enum=TRACE,enum=PATCH,enum=PROPFIND,enum=PROPPATCH,enum=MKCOL,enum=COPY,enum=MOVE,enum=LOCK,enum=UNLOCK" jsonschema_description:"The list of request methods this rule applies to."` Query [][]AccessControlRuleQuery `koanf:"query" json:"query" jsonschema:"title=Query Rules" jsonschema_description:"The list of query parameter rules this rule applies to."` @@ -37,18 +39,6 @@ type AccessControlRuleQuery struct { Value any `koanf:"value" json:"value" jsonschema:"title=Value" jsonschema_description:"The Query Parameter value for this rule."` } -// DefaultACLNetwork represents the default configuration related to access control network group configuration. -var DefaultACLNetwork = []AccessControlNetwork{ - { - Name: "localhost", - Networks: []string{"127.0.0.1"}, - }, - { - Name: "internal", - Networks: []string{"10.0.0.0/8"}, - }, -} - // DefaultACLRule represents the default configuration related to access control rule configuration. var DefaultACLRule = []AccessControlRule{ { diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go index 56d0c16ae..607dbf64b 100644 --- a/internal/configuration/schema/configuration.go +++ b/internal/configuration/schema/configuration.go @@ -27,6 +27,7 @@ type Configuration struct { PasswordPolicy PasswordPolicy `koanf:"password_policy" json:"password_policy" jsonschema:"title=Password Policy" jsonschema_description:"Password Policy Configuration."` PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy" json:"privacy_policy" jsonschema:"title=Privacy Policy" jsonschema_description:"Privacy Policy Configuration."` IdentityValidation IdentityValidation `koanf:"identity_validation" json:"identity_validation" jsonschema:"title=Identity Validation" jsonschema_description:"Identity Validation Configuration."` + Definitions Definitions `koanf:"definitions" json:"definitions" jsonschema:"title=Definitions" jsonschema_description:"Definitions for items reused elsewhere in the configuration."` // Deprecated: Use the session cookies option with the same name instead. DefaultRedirectionURL *url.URL `koanf:"default_redirection_url" json:"default_redirection_url" jsonschema:"deprecated,format=uri,title=The default redirection URL"` diff --git a/internal/configuration/schema/definitions.go b/internal/configuration/schema/definitions.go new file mode 100644 index 000000000..67297564a --- /dev/null +++ b/internal/configuration/schema/definitions.go @@ -0,0 +1,7 @@ +package schema + +import "net" + +type Definitions struct { + Network map[string][]*net.IPNet `koanf:"network" json:"network" jsonschema:"title=Network Definitions" jsonschema_description:"Networks CIDR ranges that can be utilized elsewhere in the configuration."` +} diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index bc3b07460..eb3fad7ce 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -357,5 +357,7 @@ var Keys = []string{ "identity_validation.elevated_session.characters", "identity_validation.elevated_session.require_second_factor", "identity_validation.elevated_session.skip_second_factor", + "definitions.network.*", + "definitions.network", "default_redirection_url", } diff --git a/internal/configuration/schema/types.go b/internal/configuration/schema/types.go index 8cf34f740..1f8afc0c4 100644 --- a/internal/configuration/schema/types.go +++ b/internal/configuration/schema/types.go @@ -493,12 +493,6 @@ func (RefreshIntervalDuration) JSONSchema() *jsonschema.Schema { } } -type AccessControlRuleNetworks []string - -func (AccessControlRuleNetworks) JSONSchema() *jsonschema.Schema { - return &jsonschemaWeakStringUniqueSlice -} - type IdentityProvidersOpenIDConnectClientURIs []string func (IdentityProvidersOpenIDConnectClientURIs) JSONSchema() *jsonschema.Schema { @@ -514,22 +508,6 @@ func (IdentityProvidersOpenIDConnectClientURIs) JSONSchema() *jsonschema.Schema } } -// AccessControlNetworkNetworks represents the ACL AccessControlNetworkNetworks type. -type AccessControlNetworkNetworks []string - -func (AccessControlNetworkNetworks) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - OneOf: []*jsonschema.Schema{ - &jsonschemaACLNetwork, - { - Type: jsonschema.TypeArray, - Items: &jsonschemaACLNetwork, - UniqueItems: true, - }, - }, - } -} - type AccessControlRuleDomains []string func (AccessControlRuleDomains) JSONSchema() *jsonschema.Schema { @@ -618,11 +596,6 @@ var jsonschemaWeakStringUniqueSlice = jsonschema.Schema{ }, } -var jsonschemaACLNetwork = jsonschema.Schema{ - Type: jsonschema.TypeString, - Pattern: `((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))(\/([0-2]?[0-9]|3[0-2]))?$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))?(\/(12[0-8]|1[0-1][0-9]|[0-9]{1,2}))?$))`, -} - var jsonschemaACLSubject = jsonschema.Schema{ Type: jsonschema.TypeString, Pattern: "^(user|group|oauth2:client):.+$", diff --git a/internal/configuration/schema/types_test.go b/internal/configuration/schema/types_test.go index a8fdb4bc1..a36e2aa48 100644 --- a/internal/configuration/schema/types_test.go +++ b/internal/configuration/schema/types_test.go @@ -359,8 +359,6 @@ func TestJSONSchema(t *testing.T) { &PasswordDigest{}, &TLSVersion{}, &X509CertificateChain{}, - &AccessControlRuleNetworks{}, - &AccessControlNetworkNetworks{}, &AccessControlRuleDomains{}, &AccessControlRuleMethods{}, &AccessControlRuleRegex{}, diff --git a/internal/configuration/sources.go b/internal/configuration/sources.go index bd8b559b2..c5dd1fe0d 100644 --- a/internal/configuration/sources.go +++ b/internal/configuration/sources.go @@ -20,8 +20,10 @@ import ( // accessing this path it also returns an error. func NewFileSource(path string) (source *FileSource) { return &FileSource{ - koanf: koanf.New(constDelimiter), - path: path, + koanf: koanf.New(constDelimiter), + provider: FilteredFileProvider(path), + providers: make(map[string]*FilteredFile), + path: path, } } @@ -29,9 +31,11 @@ func NewFileSource(path string) (source *FileSource) { // an issue accessing this path it also returns an error. func NewFilteredFileSource(path string, filters ...BytesFilter) (source *FileSource) { return &FileSource{ - koanf: koanf.New(constDelimiter), - path: path, - filters: filters, + koanf: koanf.New(constDelimiter), + provider: FilteredFileProvider(path, filters...), + providers: make(map[string]*FilteredFile), + path: path, + filters: filters, } } @@ -83,7 +87,7 @@ func (s *FileSource) Load(val *schema.StructValidator) (err error) { return s.loadDir(val) } - return s.koanf.Load(FilteredFileProvider(s.path, s.filters...), yaml.Parser()) + return s.koanf.Load(s.provider, yaml.Parser()) } func (s *FileSource) loadDir(_ *schema.StructValidator) (err error) { @@ -93,6 +97,11 @@ func (s *FileSource) loadDir(_ *schema.StructValidator) (err error) { return err } + var ( + provider *FilteredFile + ok bool + ) + for _, entry := range entries { if entry.IsDir() { continue @@ -100,9 +109,17 @@ func (s *FileSource) loadDir(_ *schema.StructValidator) (err error) { name := entry.Name() + file := filepath.Join(s.path, name) + switch ext := filepath.Ext(name); ext { case extYML, extYAML: - if err = s.koanf.Load(FilteredFileProvider(filepath.Join(s.path, name), s.filters...), yaml.Parser()); err != nil { + if provider, ok = s.providers[file]; !ok { + provider = FilteredFileProvider(file, s.filters...) + + s.providers[file] = provider + } + + if err = s.koanf.Load(provider, yaml.Parser()); err != nil { return err } } diff --git a/internal/configuration/test_resources/config_with_definitions.yml b/internal/configuration/test_resources/config_with_definitions.yml new file mode 100644 index 000000000..baf809893 --- /dev/null +++ b/internal/configuration/test_resources/config_with_definitions.yml @@ -0,0 +1,185 @@ +--- +default_redirection_url: 'https://home.example.com:8080/' + +server: + address: 'tcp://127.0.0.1:9091' + endpoints: + authz: + forward-auth: + implementation: 'ForwardAuth' + authn_strategies: + - name: 'HeaderProxyAuthorization' + - name: 'CookieSession' + ext-authz: + implementation: 'ExtAuthz' + authn_strategies: + - name: 'HeaderProxyAuthorization' + - name: 'CookieSession' + auth-request: + implementation: 'AuthRequest' + authn_strategies: + - name: 'HeaderAuthRequestProxyAuthorization' + - name: 'CookieSession' + legacy: + implementation: 'Legacy' + +log: + level: 'debug' + +totp: + issuer: 'authelia.com' + +duo_api: + hostname: 'api-123456789.example.com' + integration_key: 'ABCDEF' + +authentication_backend: + refresh_interval: 'disable' + ldap: + address: 'ldap://127.0.0.1' + tls: + private_key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA6z1LOg1ZCqb0lytXWZ+MRBpMHEXOoTOLYgfZXt1IYyE3Z758 + cyalk0NYQhY5cZDsXPYWPvAHiPMUxutWkoxFwby56S+AbIMa3/Is+ILrHRJs8Exn + ZkpyrYFxPX12app2kErdmAkHSx0Z5/kuXiz96PHs8S8/ZbyZolLHzdfLtSzjvRm5 + Zue5iFzsf19NJz5CIBfv8g5lRwtE8wNJoRSpn1xq7fqfuA0weDNFPzjlNWRLy6aa + rK7qJexRkmkCs4sLgyl+9NODYJpvmN8E1yhyC27E0joI6rBFVW7Ihv+cSPCdDzGp + EWe81x3AeqAa3mjVqkiq4u4Z2i8JDgBaPboqJwIDAQABAoIBAAFdLZ58jVOefDSU + L8F5R1rtvBs93GDa56f926jNJ6pLewLC+/2+757W+SAI+PRLntM7Kg3bXm/Q2QH+ + Q1Y+MflZmspbWCdI61L5GIGoYKyeers59i+FpvySj5GHtLQRiTZ0+Kv1AXHSDWBm + 9XneUOqU3IbZe0ifu1RRno72/VtjkGXbW8Mkkw+ohyGbIeTx/0/JQ6sSNZTT3Vk7 + 8i4IXptq3HSF0/vqZuah8rShoeNq72pD1YLM9YPdL5by1QkDLnqATDiCpLBTCaNV + I8sqYEun+HYbQzBj8ZACG2JVZpEEidONWQHw5BPWO95DSZYrVnEkuCqeH+u5vYt7 + CHuJ3AECgYEA+W3v5z+j91w1VPHS0VB3SCDMouycAMIUnJPAbt+0LPP0scUFsBGE + hPAKddC54pmMZRQ2KIwBKiyWfCrJ8Xz8Yogn7fJgmwTHidJBr2WQpIEkNGlK3Dzi + jXL2sh0yC7sHvn0DqiQ79l/e7yRbSnv2wrTJEczOOH2haD7/tBRyCYECgYEA8W+q + E9YyGvEltnPFaOxofNZ8LHVcZSsQI5b6fc0iE7fjxFqeXPXEwGSOTwqQLQRiHn9b + CfPmIG4Vhyq0otVmlPvUnfBZ2OK+tl5X2/mQFO3ROMdvpi0KYa994uqfJdSTaqLn + jjoKFB906UFHnDQDLZUNiV1WwnkTglgLc+xrd6cCgYEAqqthyv6NyBTM3Tm2gcio + Ra9Dtntl51LlXZnvwy3IkDXBCd6BHM9vuLKyxZiziGx+Vy90O1xI872cnot8sINQ + Am+dur/tAEVN72zxyv0Y8qb2yfH96iKy9gxi5s75TnOEQgAygLnYWaWR2lorKRUX + bHTdXBOiS58S0UzCFEslGIECgYBqkO4SKWYeTDhoKvuEj2yjRYyzlu28XeCWxOo1 + otiauX0YSyNBRt2cSgYiTzhKFng0m+QUJYp63/wymB/5C5Zmxi0XtWIDADpLhqLj + HmmBQ2Mo26alQ5YkffBju0mZyhVzaQop1eZi8WuKFV1FThPlB7hc3E0SM5zv2Grd + tQnOWwKBgQC40yZY0PcjuILhy+sIc0Wvh7LUA7taSdTye149kRvbvsCDN7Jh75lM + USjhLXY0Nld2zBm9r8wMb81mXH29uvD+tDqqsICvyuKlA/tyzXR+QTr7dCVKVwu0 + 1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== + -----END RSA PRIVATE KEY----- + base_dn: 'dc=example,dc=com' + additional_users_dn: 'ou=users' + users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' + additional_groups_dn: 'ou=groups' + groups_filter: '(&(member={dn})(objectClass=groupOfNames))' + user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' + +definitions: + network: + lan: + - '192.168.1.0/24' + - '192.168.2.0/24' + +access_control: + default_policy: 'deny' + networks: + - name: 'abc' + networks: + - '192.168.3.0/24' + - '192.168.4.0/24' + rules: + # Rules applied to everyone + - domain: 'public.example.com' + policy: 'bypass' + + - domain: 'secure.example.com' + policy: 'one_factor' + # Network based rule, if not provided any network matches. + networks: + - '192.168.0.0/24' + - 'lan' + - 'abc' + - domain: 'secure.example.com' + policy: 'two_factor' + + - domain: ['singlefactor.example.com', 'onefactor.example.com'] + policy: 'one_factor' + + # Rules applied to 'admins' group + - domain: 'mx2.mail.example.com' + subject: 'group:admins' + policy: 'deny' + - domain: '*.example.com' + subject: 'group:admins' + policy: 'two_factor' + + # Rules applied to 'dev' group + - domain: 'dev.example.com' + resources: + - '^/groups/dev/.*$' + subject: 'group:dev' + policy: 'two_factor' + + # Rules applied to user 'john' + - domain: 'dev.example.com' + resources: + - '^/users/john/.*$' + subject: 'user:john' + policy: 'two_factor' + + # Rules applied to 'dev' group and user 'john' + - domain: 'dev.example.com' + resources: + - '^/deny-all.*$' + subject: ['group:dev', 'user:john'] + policy: 'deny' + + # Rules applied to user 'harry' + - domain: 'dev.example.com' + resources: + - '^/users/harry/.*$' + subject: 'user:harry' + policy: 'two_factor' + + # Rules applied to user 'bob' + - domain: '*.mail.example.com' + subject: 'user:bob' + policy: 'two_factor' + - domain: 'dev.example.com' + resources: + - '^/users/bob/.*$' + subject: 'user:bob' + policy: 'two_factor' + +session: + name: 'authelia_session' + expiration: '1h' # 1 hour + inactivity: '5m' # 5 minutes + domain: 'example.com' + redis: + host: '127.0.0.1' + port: 6379 + high_availability: + sentinel_name: 'test' + +regulation: + max_retries: 3 + find_time: '2m' + ban_time: '5m' + +storage: + mysql: + address: 'tcp://127.0.0.1:3306' + database: 'authelia' + username: 'authelia' + +notifier: + smtp: + address: 'smtp://127.0.0.1:1025' + username: 'test' + sender: 'admin@example.com' + disable_require_tls: true +... diff --git a/internal/configuration/test_resources/decode_networks.yml b/internal/configuration/test_resources/decode_networks.yml new file mode 100644 index 000000000..2c1e1046c --- /dev/null +++ b/internal/configuration/test_resources/decode_networks.yml @@ -0,0 +1,8 @@ +--- +definitions: + network: + single: '192.168.0.1' + example: + - '192.168.1.20' + - '192.168.2.0/24' +... diff --git a/internal/configuration/test_resources/decode_networks_abc.yml b/internal/configuration/test_resources/decode_networks_abc.yml new file mode 100644 index 000000000..fcdf730a7 --- /dev/null +++ b/internal/configuration/test_resources/decode_networks_abc.yml @@ -0,0 +1,8 @@ +--- +definitions: + network: + example: + - '192.168.1.20' + - '192.168.2.0/24' + - 'abc' +... diff --git a/internal/configuration/types.go b/internal/configuration/types.go index d1c487a2f..0a7c3d106 100644 --- a/internal/configuration/types.go +++ b/internal/configuration/types.go @@ -16,9 +16,11 @@ type Source interface { // FileSource is a file configuration.Source. type FileSource struct { - koanf *koanf.Koanf - path string - filters []BytesFilter + koanf *koanf.Koanf + provider *FilteredFile + providers map[string]*FilteredFile + path string + filters []BytesFilter } // BytesSource is a raw bytes configuration.Source. diff --git a/internal/configuration/validator/access_control.go b/internal/configuration/validator/access_control.go index aa392351e..86aa37996 100644 --- a/internal/configuration/validator/access_control.go +++ b/internal/configuration/validator/access_control.go @@ -2,7 +2,6 @@ package validator import ( "fmt" - "net" "regexp" "strings" @@ -11,51 +10,6 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) -// IsPolicyValid check if policy is valid. -func IsPolicyValid(policy string) (isValid bool) { - return utils.IsStringInSlice(policy, validACLRulePolicies) -} - -// IsSubjectValid check if a subject is valid. -func IsSubjectValid(subject string) (isValid bool) { - return subject == "" || IsSubjectValidStrict(subject) || strings.HasPrefix(subject, "oauth2:client:") -} - -func IsSubjectValidStrict(subject string) (isValid bool) { - return strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:") -} - -// IsNetworkGroupValid check if a network group is valid. -func IsNetworkGroupValid(config schema.AccessControl, network string) bool { - for _, networks := range config.Networks { - if network != networks.Name { - continue - } else { - return true - } - } - - return false -} - -// IsNetworkValid checks if a network is valid. -func IsNetworkValid(network string) (isValid bool) { - if net.ParseIP(network) == nil { - _, _, err := net.ParseCIDR(network) - return err == nil - } - - return true -} - -func ruleDescriptor(position int, rule schema.AccessControlRule) string { - if len(rule.Domains) == 0 { - return fmt.Sprintf("#%d", position) - } - - return fmt.Sprintf("#%d (domain '%s')", position, strings.Join(rule.Domains, ",")) -} - // ValidateAccessControl validates access control configuration. func ValidateAccessControl(config *schema.Configuration, validator *schema.StructValidator) { if config.AccessControl.DefaultPolicy == "" { @@ -65,14 +19,6 @@ func ValidateAccessControl(config *schema.Configuration, validator *schema.Struc if !IsPolicyValid(config.AccessControl.DefaultPolicy) { validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyValue, utils.StringJoinOr(validACLRulePolicies), config.AccessControl.DefaultPolicy)) } - - for _, n := range config.AccessControl.Networks { - for _, networks := range n.Networks { - if !IsNetworkValid(networks) { - validator.Push(fmt.Errorf(errFmtAccessControlNetworkGroupIPCIDRInvalid, n.Name, networks)) - } - } - } } // ValidateRules validates an ACL Rule configuration. @@ -103,9 +49,7 @@ func ValidateRules(config *schema.Configuration, validator *schema.StructValidat } } - validateNetworks(rulePosition, rule, config.AccessControl, validator) - - validateSubjects(rulePosition, rule, validator) + validateSubjects(rulePosition, rule, config, validator) validateMethods(rulePosition, rule, validator) @@ -142,21 +86,22 @@ func validateDomains(rulePosition int, rule schema.AccessControlRule, validator } } -func validateNetworks(rulePosition int, rule schema.AccessControlRule, config schema.AccessControl, validator *schema.StructValidator) { - for _, network := range rule.Networks { - if !IsNetworkValid(network) { - if !IsNetworkGroupValid(config, network) { - validator.Push(fmt.Errorf(errFmtAccessControlRuleNetworksInvalid, ruleDescriptor(rulePosition, rule), network)) - } - } - } -} +func validateSubjects(rulePosition int, rule schema.AccessControlRule, config *schema.Configuration, validator *schema.StructValidator) { + var ( + id string + isValid bool + ) -func validateSubjects(rulePosition int, rule schema.AccessControlRule, validator *schema.StructValidator) { for _, subjectRule := range rule.Subjects { for _, subject := range subjectRule { - if !IsSubjectValid(subject) { + if id, isValid = IsSubjectValid(subject); !isValid { validator.Push(fmt.Errorf(errFmtAccessControlRuleSubjectInvalid, ruleDescriptor(rulePosition, rule), subject)) + + continue + } + + if len(id) != 0 && !IsSubjectValidOAuth20(config, id) { + validator.Push(fmt.Errorf(errFmtAccessControlRuleOAuth2ClientSubjectInvalid, ruleDescriptor(rulePosition, rule), subject, id)) } } } @@ -230,3 +175,47 @@ func validateQuery(i int, rule schema.AccessControlRule, config *schema.Configur } } } + +// IsPolicyValid check if policy is valid. +func IsPolicyValid(policy string) (isValid bool) { + return utils.IsStringInSlice(policy, validACLRulePolicies) +} + +// IsSubjectValid validates if a subject has a valid prefix and returns the client id if applicable. +func IsSubjectValid(subject string) (id string, isValid bool) { + if IsSubjectValidBasic(subject) { + return "", true + } + + if strings.HasPrefix(subject, "oauth2:client:") { + return strings.TrimPrefix(subject, "oauth2:client:"), true + } + + return "", false +} + +func IsSubjectValidBasic(subject string) (isValid bool) { + return strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:") +} + +func IsSubjectValidOAuth20(config *schema.Configuration, id string) (isValid bool) { + if config.IdentityProviders.OIDC == nil || len(config.IdentityProviders.OIDC.Clients) == 0 { + return false + } + + for _, client := range config.IdentityProviders.OIDC.Clients { + if client.ID == id { + return true + } + } + + return false +} + +func ruleDescriptor(position int, rule schema.AccessControlRule) string { + if len(rule.Domains) == 0 { + return fmt.Sprintf("#%d", position) + } + + return fmt.Sprintf("#%d (domain '%s')", position, strings.Join(rule.Domains, ",")) +} diff --git a/internal/configuration/validator/access_control_test.go b/internal/configuration/validator/access_control_test.go index ad38ec9a0..b288ab9ad 100644 --- a/internal/configuration/validator/access_control_test.go +++ b/internal/configuration/validator/access_control_test.go @@ -23,9 +23,7 @@ func (suite *AccessControl) SetupTest() { suite.config = &schema.Configuration{ AccessControl: schema.AccessControl{ DefaultPolicy: policyDeny, - - Networks: schema.DefaultACLNetwork, - Rules: schema.DefaultACLRule, + Rules: schema.DefaultACLRule, }, } } @@ -73,22 +71,6 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() { suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: option 'default_policy' must be one of 'bypass', 'one_factor', 'two_factor', or 'deny' but it's configured as 'invalid'") } -func (suite *AccessControl) TestShouldRaiseErrorInvalidNetworkGroupNetwork() { - suite.config.AccessControl.Networks = []schema.AccessControlNetwork{ - { - Name: "internal", - Networks: []string{"abc.def.ghi.jkl"}, - }, - } - - ValidateAccessControl(suite.config, suite.validator) - - suite.Assert().Len(suite.validator.Warnings(), 0) - suite.Require().Len(suite.validator.Errors(), 1) - - suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: networks: network group 'internal' is invalid: the network 'abc.def.ghi.jkl' is not a valid IP or CIDR notation") -} - func (suite *AccessControl) TestShouldRaiseWarningOnBadDomain() { suite.config.AccessControl.Rules = []schema.AccessControlRule{ { @@ -164,23 +146,6 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() { suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: rule #1 (domain 'public.example.com'): option 'policy' must be one of 'bypass', 'one_factor', 'two_factor', or 'deny' but it's configured as 'invalid'") } -func (suite *AccessControl) TestShouldRaiseErrorInvalidNetwork() { - suite.config.AccessControl.Rules = []schema.AccessControlRule{ - { - Domains: []string{"public.example.com"}, - Policy: "bypass", - Networks: []string{"abc.def.ghi.jkl/32"}, - }, - } - - ValidateRules(suite.config, suite.validator) - - suite.Assert().Len(suite.validator.Warnings(), 0) - suite.Require().Len(suite.validator.Errors(), 1) - - suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: rule #1 (domain 'public.example.com'): the network 'abc.def.ghi.jkl/32' is not a valid Group Name, IP, or CIDR notation") -} - func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() { suite.config.AccessControl.Rules = []schema.AccessControlRule{ { @@ -231,10 +196,105 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() { suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 2) - suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: rule #1 (domain 'public.example.com'): 'subject' option 'invalid' is invalid: must start with 'user:' or 'group:'") + suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: rule #1 (domain 'public.example.com'): 'subject' option 'invalid' is invalid: must start with 'user:', 'group:', or 'oauth2:client:'") suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0]))) } +func (suite *AccessControl) TestShouldValidateClientIDSubjectWithoutClient() { + domains := []string{"public.example.com"} + subjects := [][]string{{"oauth2:client:example"}} + suite.config.AccessControl.Rules = []schema.AccessControlRule{ + { + Domains: domains, + Policy: "bypass", + Subjects: subjects, + }, + } + + ValidateRules(suite.config, suite.validator) + + suite.Require().Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 2) + + suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: rule #1 (domain 'public.example.com'): option 'subject' with value 'oauth2:client:example' is invalid: the client id 'example' does not belong to a registered client") + suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0]))) +} + +func (suite *AccessControl) TestShouldValidateClientIDSubjectWithoutClientMatchingID() { + domains := []string{"public.example.com"} + subjects := [][]string{{"oauth2:client:example"}} + suite.config.IdentityProviders.OIDC = &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "example2", + }, + }, + } + suite.config.AccessControl.Rules = []schema.AccessControlRule{ + { + Domains: domains, + Policy: "bypass", + Subjects: subjects, + }, + } + + ValidateRules(suite.config, suite.validator) + + suite.Require().Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 2) + + suite.Assert().EqualError(suite.validator.Errors()[0], "access_control: rule #1 (domain 'public.example.com'): option 'subject' with value 'oauth2:client:example' is invalid: the client id 'example' does not belong to a registered client") + suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0]))) +} + +func (suite *AccessControl) TestShouldValidateClientIDSubject() { + domains := []string{"public.example.com"} + subjects := [][]string{{"oauth2:client:example"}} + suite.config.IdentityProviders.OIDC = &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "example", + }, + }, + } + suite.config.AccessControl.Rules = []schema.AccessControlRule{ + { + Domains: domains, + Policy: "one_factor", + Subjects: subjects, + }, + } + + ValidateRules(suite.config, suite.validator) + + suite.Require().Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 0) +} + +func (suite *AccessControl) TestShouldValidateBasicSubject() { + domains := []string{"public.example.com"} + subjects := [][]string{{"user:example"}} + suite.config.IdentityProviders.OIDC = &schema.IdentityProvidersOpenIDConnect{ + Clients: []schema.IdentityProvidersOpenIDConnectClient{ + { + ID: "example", + }, + }, + } + suite.config.AccessControl.Rules = []schema.AccessControlRule{ + { + Domains: domains, + Policy: "one_factor", + Subjects: subjects, + }, + } + + ValidateRules(suite.config, suite.validator) + + suite.Require().Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 0) +} + func (suite *AccessControl) TestShouldRaiseErrorBypassWithSubjectDomainRegexGroup() { suite.config.AccessControl.Rules = []schema.AccessControlRule{ { @@ -398,18 +458,6 @@ func TestAccessControl(t *testing.T) { suite.Run(t, new(AccessControl)) } -func TestShouldReturnCorrectResultsForValidNetworkGroups(t *testing.T) { - config := schema.AccessControl{ - Networks: schema.DefaultACLNetwork, - } - - validNetwork := IsNetworkGroupValid(config, "internal") - invalidNetwork := IsNetworkGroupValid(config, loopback) - - assert.True(t, validNetwork) - assert.False(t, invalidNetwork) -} - func MustCompileRegexps(exps []string) (regexps []regexp.Regexp) { regexps = make([]regexp.Regexp, len(exps)) diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 26c057495..ebc83c380 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -333,7 +333,9 @@ const ( errFmtAccessControlRuleNetworksInvalid = "access_control: rule %s: the network '%s' is not a " + "valid Group Name, IP, or CIDR notation" errFmtAccessControlRuleSubjectInvalid = "access_control: rule %s: 'subject' option '%s' is " + - "invalid: must start with 'user:' or 'group:'" + "invalid: must start with 'user:', 'group:', or 'oauth2:client:'" + errFmtAccessControlRuleOAuth2ClientSubjectInvalid = "access_control: rule %s: option 'subject' with value '%s' is " + + "invalid: the client id '%s' does not belong to a registered client" errFmtAccessControlRuleInvalidEntries = "access_control: rule %s: option '%s' must only have the values %s but the values %s are present" errFmtAccessControlRuleInvalidDuplicates = "access_control: rule %s: option '%s' must have unique values but the values %s are duplicated" errFmtAccessControlRuleQueryInvalid = "access_control: rule %s: query: option 'operator' must be one of %s but it's configured as '%s'" diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index b168e7d71..5a75f8460 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -115,7 +115,7 @@ func validateOIDCAuthorizationPoliciesPolicyRules(i int, policy string, config * for _, subjects := range config.AuthorizationPolicies[policy].Rules[i].Subjects { for _, subject := range subjects { - if !IsSubjectValidStrict(subject) { + if !IsSubjectValidBasic(subject) { validator.Push(fmt.Errorf(errFmtOIDCPolicyRuleSubjectInvalid, policy, i+1, subject)) n = -1 diff --git a/internal/configuration/validator/keys.go b/internal/configuration/validator/keys.go index f53f8ee5a..ee2179135 100644 --- a/internal/configuration/validator/keys.go +++ b/internal/configuration/validator/keys.go @@ -78,7 +78,7 @@ func NewKeyPattern(key string) (pattern *regexp.Regexp, err error) { } } -var reIsMapKey = regexp.MustCompile(`\.\*(\[]|\.)`) +var reIsMapKey = regexp.MustCompile(`\.\*(\[]|\.|$)`) // NewKeyMapPattern returns a pattern required to match map keys. func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) { @@ -92,6 +92,10 @@ func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) { for i, part := range parts { if i != 0 && !strings.HasPrefix(part, "[]") { + if len(parts) == i+1 && strings.HasSuffix(key, ".*") { + continue + } + buf.WriteString("\\.") } @@ -111,7 +115,7 @@ func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) { } if i < n { - buf.WriteString("\\.[a-z0-9](([a-z0-9-_/]+)?[a-z0-9])?") + buf.WriteString("\\.[a-zA-Z0-9](([a-zA-Z0-9/_-]+)?[a-zA-Z0-9])?") } } diff --git a/internal/configuration/validator/storage_test.go b/internal/configuration/validator/storage_test.go index 29f9d42c4..fae3ab681 100644 --- a/internal/configuration/validator/storage_test.go +++ b/internal/configuration/validator/storage_test.go @@ -116,6 +116,24 @@ func (suite *StorageSuite) TestShouldSetDefaultMySQLTLSServerName() { suite.Assert().Equal("mysql", suite.config.MySQL.TLS.ServerName) } +func (suite *StorageSuite) TestShouldRaiseErrorOnInvalidMySQLAddressScheme() { + suite.config.MySQL = &schema.StorageMySQL{ + StorageSQL: schema.StorageSQL{ + Address: &schema.AddressTCP{Address: MustParseAddress("udp://mysql:1234")}, + Username: "myuser", + Password: "pass", + Database: "database", + }, + } + + ValidateStorage(suite.config, suite.val) + + suite.Len(suite.val.Warnings(), 0) + suite.Require().Len(suite.val.Errors(), 1) + + suite.EqualError(suite.val.Errors()[0], "server: option 'address' with value 'udp://mysql:1234' is invalid: scheme must be one of 'tcp', 'tcp4', 'tcp6', or 'unix' but is configured as 'udp'") +} + func (suite *StorageSuite) TestShouldRaiseErrorOnInvalidMySQLTLSVersion() { suite.config.MySQL = &schema.StorageMySQL{ StorageSQL: schema.StorageSQL{ @@ -213,6 +231,27 @@ func (suite *StorageSuite) TestShouldValidatePostgresSchemaDefault() { suite.Assert().Equal("public", suite.config.PostgreSQL.Schema) } +func (suite *StorageSuite) TestShouldValidatePostgresPortDefault() { + suite.config.PostgreSQL = &schema.StoragePostgreSQL{ + StorageSQL: schema.StorageSQL{ + Address: &schema.AddressTCP{ + Address: MustParseAddress("tcp://postgre"), + }, + Username: "myuser", + Password: "pass", + Database: "database", + }, + Schema: "public", + } + + ValidateStorage(suite.config, suite.val) + + suite.Assert().Len(suite.val.Warnings(), 0) + suite.Assert().Len(suite.val.Errors(), 0) + + suite.Equal(uint16(5432), suite.config.PostgreSQL.Address.Port()) +} + func (suite *StorageSuite) TestShouldValidatePostgresTLSDefaults() { suite.config.PostgreSQL = &schema.StoragePostgreSQL{ StorageSQL: schema.StorageSQL{ diff --git a/internal/configuration/validator/util_test.go b/internal/configuration/validator/util_test.go index a41c1d138..49ff521ec 100644 --- a/internal/configuration/validator/util_test.go +++ b/internal/configuration/validator/util_test.go @@ -11,6 +11,12 @@ import ( "github.com/authelia/authelia/v4/internal/oidc" ) +func TestMiscMissingCoverage(t *testing.T) { + kid, err := jwkCalculateKID(struct{}{}, nil, "") + assert.NoError(t, err) + assert.Equal(t, "", kid) +} + func TestIsCookieDomainValid(t *testing.T) { testCases := []struct { domain string diff --git a/internal/utils/ip.go b/internal/utils/ip.go new file mode 100644 index 000000000..c5547fda5 --- /dev/null +++ b/internal/utils/ip.go @@ -0,0 +1,32 @@ +package utils + +import ( + "net" + "strings" +) + +// ParseHostCIDR parses a raw string as a *net.IPNet similar to net.ParseCIDR, in fact it leverages it. The only +// differences between the functions is if the input does not contain a single '/' it first parses it with net.ParseIP +// to determine if it's a IPv4 or IPv6 and then adds the relevant CIDR suffix for a single host, and it only returns the +// *net.IPNet and error, discarding the net.IP. +func ParseHostCIDR(s string) (cidr *net.IPNet, err error) { + switch strings.Count(s, "/") { + case 1: + _, cidr, err = net.ParseCIDR(s) + + return + case 0: + switch n := net.ParseIP(s); { + case n == nil: + return nil, &net.ParseError{Type: "CIDR address", Text: s} + case n.To4() == nil: + _, cidr, err = net.ParseCIDR(s + "/128") + default: + _, cidr, err = net.ParseCIDR(s + "/32") + } + + return + default: + return nil, &net.ParseError{Type: "CIDR address", Text: s} + } +} diff --git a/internal/utils/ip_test.go b/internal/utils/ip_test.go new file mode 100644 index 000000000..8b63754fc --- /dev/null +++ b/internal/utils/ip_test.go @@ -0,0 +1,128 @@ +package utils + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseHostCIDR(t *testing.T) { + mustParse := func(in string) *net.IPNet { + _, out, err := net.ParseCIDR(in) + require.NoError(t, err) + + return out + } + + testCases := []struct { + name string + have string + expected *net.IPNet + err string + }{ + { + "ShouldParseIPv4", + "192.168.1.1", + mustParse("192.168.1.1/32"), + "", + }, + { + "ShouldParseIPv4Zero", + "0.0.0.0", + mustParse("0.0.0.0/32"), + "", + }, + { + "ShouldParseIPv4ZeroWithZeroCIDR", + "0.0.0.0/0", + mustParse("0.0.0.0/0"), + "", + }, + { + "ShouldParseIPv6", + "2001:db8:3333:4444:5555:6666:7777:8888", + mustParse("2001:db8:3333:4444:5555:6666:7777:8888/128"), + "", + }, + { + "ShouldParseIPv4WithCIDR", + "192.168.1.1/24", + mustParse("192.168.1.0/24"), + "", + }, + { + "ShouldParseIPv6WithCIDR", + "2001:db8:3333:4444:5555:6666:7777:8888/56", + mustParse("2001:db8:3333:4400::/56"), + "", + }, + { + "ShouldParseIPv6Zero", + "::1", + mustParse("::1/128"), + "", + }, + { + "ShouldParseIPv6ZeroWithCIDR", + "::1/0", + mustParse("::1/0"), + "", + }, + { + "ShouldNotParseMultipleSlashes", + "192.168.1.1/24/0", + nil, + "invalid CIDR address: 192.168.1.1/24/0", + }, + { + "ShouldNotParseString", + "abc", + nil, + "invalid CIDR address: abc", + }, + { + "ShouldNotParseEmptyString", + "", + nil, + "invalid CIDR address: ", + }, + { + "ShouldNotParseInvalidIPv4Format", + "256.256.256.256", + nil, + "invalid CIDR address: 256.256.256.256", + }, + { + "ShouldNotParseIPv4IncompleteOctets", + "192.168.1", + nil, + "invalid CIDR address: 192.168.1", + }, + { + "ShouldNotParseIPv6InvalidCIDR", + "2001:db8::1/129", + nil, + "invalid CIDR address: 2001:db8::1/129", + }, + { + "ShouldNotParseIPv4InvalidCIDR", + "192.168.1.1/33", + nil, + "invalid CIDR address: 192.168.1.1/33", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := ParseHostCIDR(tc.have) + if tc.err != "" { + assert.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + } + }) + } +} |
