perf(server): cached openapi document (#4674)
This should lead to a small performance gain by caching the openapi.yml with etags as well as eliminating the use of nonce crypto generation when not required.pull/4682/head
parent
acaadd81cb
commit
1c3219e93f
|
@ -9,6 +9,7 @@ yaml-files:
|
|||
- '.yamllint'
|
||||
|
||||
ignore: |
|
||||
api/openapi.yml
|
||||
docs/pnpm-lock.yaml
|
||||
internal/configuration/test_resources/config_bad_quoting.yml
|
||||
web/pnpm-lock.yaml
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{.Base}}/api/swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="{{.Base}}/api/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="{{.Base}}/api/favicon-16x16.png" sizes="16x16" />
|
||||
<style nonce="{{.CSPNonce}}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ .Base }}/api/swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="{{ .Base }}/api/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="{{ .Base }}/api/favicon-16x16.png" sizes="16x16" />
|
||||
<style nonce="{{ .CSPNonce }}">
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
|
@ -33,13 +33,13 @@
|
|||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="{{.Base}}/api/swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="{{.Base}}/api/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script nonce="{{.CSPNonce}}">
|
||||
<script src="{{ .Base }}/api/swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="{{ .Base }}/api/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script nonce="{{ .CSPNonce }}">
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "{{.Base}}/api/openapi.yml",
|
||||
url: "{{ .Base }}/api/openapi.yml",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
|
|
454
api/openapi.yml
454
api/openapi.yml
|
@ -1,4 +1,3 @@
|
|||
# yamllint disable rule:line-length
|
||||
---
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
|
@ -22,18 +21,24 @@ tags:
|
|||
description: Configuration, health and state endpoints
|
||||
- name: Authentication
|
||||
description: Authentication and verification endpoints
|
||||
{{- if .PasswordReset }}
|
||||
- name: Password Reset
|
||||
description: Password reset endpoints
|
||||
- name: User Information
|
||||
description: User configuration endpoints
|
||||
{{- end }}
|
||||
{{- if (or .TOTP .Webauthn .Duo) }}
|
||||
- name: Second Factor
|
||||
description: TOTP, Webauthn and Duo endpoints
|
||||
externalDocs:
|
||||
url: https://www.authelia.com/configuration/second-factor/introduction/
|
||||
{{- end }}
|
||||
{{- if .OpenIDConnect }}
|
||||
- name: OpenID Connect 1.0
|
||||
description: OpenID Connect 1.0 and OAuth 2.0 Endpoints
|
||||
externalDocs:
|
||||
url: https://www.authelia.com/integration/openid-connect/introduction/
|
||||
{{- end }}
|
||||
paths:
|
||||
/api/configuration:
|
||||
get:
|
||||
|
@ -97,280 +102,8 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/handlers.StateResponse'
|
||||
/api/verify:
|
||||
get:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
head:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
options:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
put:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
patch:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
delete:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
description: >
|
||||
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
|
||||
domain.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/originalURLParam'
|
||||
- $ref: '#/components/parameters/forwardedMethodParam'
|
||||
- $ref: '#/components/parameters/authParam'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
headers:
|
||||
remote-user:
|
||||
description: Username
|
||||
schema:
|
||||
type: string
|
||||
example: john
|
||||
remote-name:
|
||||
description: Name
|
||||
schema:
|
||||
type: string
|
||||
example: John Doe
|
||||
remote-email:
|
||||
description: Email
|
||||
schema:
|
||||
type: string
|
||||
example: john.doe@authelia.com
|
||||
remote-groups:
|
||||
description: Comma separated list of Groups
|
||||
schema:
|
||||
type: string
|
||||
example: admin,devs
|
||||
"401":
|
||||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
trace:
|
||||
{{- range $method := list "get" "head" "options" "post" "put" "patch" "delete" "trace" }}
|
||||
{{ $method }}:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Verification
|
||||
|
@ -409,6 +142,7 @@ paths:
|
|||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- end }}
|
||||
/api/firstfactor:
|
||||
post:
|
||||
tags:
|
||||
|
@ -477,6 +211,7 @@ paths:
|
|||
$ref: '#/components/schemas/handlers.logoutResponseBody'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- if .PasswordReset }}
|
||||
/api/reset-password/identity/start:
|
||||
post:
|
||||
tags:
|
||||
|
@ -494,7 +229,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/handlers.resetPasswordStep1RequestBody'
|
||||
$ref: '#/components/schemas/handlers.PasswordResetStep1RequestBody'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
|
@ -546,7 +281,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/handlers.resetPasswordStep2RequestBody'
|
||||
$ref: '#/components/schemas/handlers.PasswordResetStep2RequestBody'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
|
@ -556,6 +291,7 @@ paths:
|
|||
$ref: '#/components/schemas/middlewares.OkResponse'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- end }}
|
||||
/api/user/info:
|
||||
get:
|
||||
tags:
|
||||
|
@ -593,25 +329,6 @@ paths:
|
|||
description: Forbidden
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/user/info/totp:
|
||||
get:
|
||||
tags:
|
||||
- User Information
|
||||
summary: User TOTP Configuration
|
||||
description: >
|
||||
The user TOTP info endpoint provides information necessary to display the TOTP component to validate their
|
||||
TOTP input such as the period/frequency and number of digits.
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/handlers.UserInfoTOTP'
|
||||
"403":
|
||||
description: Forbidden
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/user/info/2fa_method:
|
||||
post:
|
||||
tags:
|
||||
|
@ -634,6 +351,26 @@ paths:
|
|||
description: Forbidden
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- if .TOTP }}
|
||||
/api/user/info/totp:
|
||||
get:
|
||||
tags:
|
||||
- User Information
|
||||
summary: User TOTP Configuration
|
||||
description: >
|
||||
The user TOTP info endpoint provides information necessary to display the TOTP component to validate their
|
||||
TOTP input such as the period/frequency and number of digits.
|
||||
responses:
|
||||
"200":
|
||||
description: Successful Operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/handlers.UserInfoTOTP'
|
||||
"403":
|
||||
description: Forbidden
|
||||
security:
|
||||
- authelia_auth: []
|
||||
/api/secondfactor/totp/identity/start:
|
||||
post:
|
||||
tags:
|
||||
|
@ -706,6 +443,8 @@ paths:
|
|||
$ref: '#/components/schemas/middlewares.ErrorResponse'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- end }}
|
||||
{{- if .Webauthn }}
|
||||
/api/secondfactor/webauthn/assertion:
|
||||
get:
|
||||
tags:
|
||||
|
@ -812,6 +551,8 @@ paths:
|
|||
$ref: '#/components/schemas/middlewares.OkResponse'
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- end }}
|
||||
{{- if .Duo }}
|
||||
/api/secondfactor/duo:
|
||||
post:
|
||||
tags:
|
||||
|
@ -875,6 +616,8 @@ paths:
|
|||
description: Unauthorized
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- end }}
|
||||
{{- if .OpenIDConnect }}
|
||||
/.well-known/openid-configuration:
|
||||
get:
|
||||
tags:
|
||||
|
@ -1389,6 +1132,7 @@ paths:
|
|||
description: Forbidden
|
||||
security:
|
||||
- authelia_auth: []
|
||||
{{- end }}
|
||||
components:
|
||||
parameters:
|
||||
originalURLParam:
|
||||
|
@ -1609,7 +1353,8 @@ components:
|
|||
redirect:
|
||||
type: string
|
||||
example: https://home.example.com
|
||||
handlers.resetPasswordStep1RequestBody:
|
||||
{{- if .PasswordReset }}
|
||||
handlers.PasswordResetStep1RequestBody:
|
||||
required:
|
||||
- username
|
||||
type: object
|
||||
|
@ -1617,7 +1362,7 @@ components:
|
|||
username:
|
||||
type: string
|
||||
example: john
|
||||
handlers.resetPasswordStep2RequestBody:
|
||||
handlers.PasswordResetStep2RequestBody:
|
||||
required:
|
||||
- password
|
||||
type: object
|
||||
|
@ -1625,6 +1370,8 @@ components:
|
|||
password:
|
||||
type: string
|
||||
example: password
|
||||
{{- end }}
|
||||
{{- if .Duo }}
|
||||
handlers.bodySignDuoRequest:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1641,23 +1388,7 @@ components:
|
|||
format: uuid
|
||||
pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
|
||||
example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c"
|
||||
handlers.bodySignTOTPRequest:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
example: "123456"
|
||||
targetURL:
|
||||
type: string
|
||||
example: https://secure.example.com
|
||||
workflow:
|
||||
type: string
|
||||
example: openid_connect
|
||||
workflowID:
|
||||
type: string
|
||||
format: uuid
|
||||
pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
|
||||
example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c"
|
||||
{{- end }}
|
||||
handlers.StateResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1676,7 +1407,24 @@ components:
|
|||
default_redirection_url:
|
||||
type: string
|
||||
example: https://home.example.com
|
||||
handlers.TOTPKeyResponse:
|
||||
middlewares.ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: KO
|
||||
message:
|
||||
type: string
|
||||
example: Authentication failed, please retry later.
|
||||
middlewares.IdentityVerificationFinishBody:
|
||||
required:
|
||||
- token
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDc5MjU1OTYsImlzcyI6IkF1dGhlbGlhIiwiYWN0aW9uIjoiUmVzZXRQYXNzd29yZCIsInVzZXJuYW1lIjoiQW1pciJ9.636yqRrUCGCe4jsMCsonleX5CYWHncYqZum-YYb6VaY
|
||||
middlewares.OkResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
|
@ -1684,13 +1432,6 @@ components:
|
|||
example: OK
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
base32_secret:
|
||||
type: string
|
||||
example: 5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
|
||||
otpauth_url:
|
||||
type: string
|
||||
example: otpauth://totp/auth.example.com:john?algorithm=SHA1&digits=6&issuer=auth.example.com&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
|
||||
handlers.UserInfo:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1719,6 +1460,19 @@ components:
|
|||
has_duo:
|
||||
type: boolean
|
||||
example: true
|
||||
handlers.UserInfo.MethodBody:
|
||||
required:
|
||||
- method
|
||||
type: object
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
enum:
|
||||
- "totp"
|
||||
- "webauthn"
|
||||
- "mobile_push"
|
||||
example: totp
|
||||
{{- if .TOTP }}
|
||||
handlers.UserInfoTOTP:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1738,36 +1492,24 @@ components:
|
|||
description: The number of digits defined in the users TOTP configuration
|
||||
type: integer
|
||||
example: 6
|
||||
handlers.UserInfo.MethodBody:
|
||||
required:
|
||||
- method
|
||||
type: object
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
enum:
|
||||
- "totp"
|
||||
- "webauthn"
|
||||
- "mobile_push"
|
||||
example: totp
|
||||
middlewares.ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: KO
|
||||
message:
|
||||
type: string
|
||||
example: Authentication failed, please retry later.
|
||||
middlewares.IdentityVerificationFinishBody:
|
||||
required:
|
||||
- token
|
||||
handlers.bodySignTOTPRequest:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDc5MjU1OTYsImlzcyI6IkF1dGhlbGlhIiwiYWN0aW9uIjoiUmVzZXRQYXNzd29yZCIsInVzZXJuYW1lIjoiQW1pciJ9.636yqRrUCGCe4jsMCsonleX5CYWHncYqZum-YYb6VaY
|
||||
middlewares.OkResponse:
|
||||
example: "123456"
|
||||
targetURL:
|
||||
type: string
|
||||
example: https://secure.example.com
|
||||
workflow:
|
||||
type: string
|
||||
example: openid_connect
|
||||
workflowID:
|
||||
type: string
|
||||
format: uuid
|
||||
pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
|
||||
example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c"
|
||||
handlers.TOTPKeyResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
|
@ -1775,6 +1517,15 @@ components:
|
|||
example: OK
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
base32_secret:
|
||||
type: string
|
||||
example: 5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
|
||||
otpauth_url:
|
||||
type: string
|
||||
example: otpauth://totp/auth.example.com:john?algorithm=SHA1&digits=6&issuer=auth.example.com&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
|
||||
{{- end }}
|
||||
{{- if .Webauthn }}
|
||||
webauthn.PublicKeyCredential:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -2073,6 +1824,8 @@ components:
|
|||
written:
|
||||
type: boolean
|
||||
example: false
|
||||
{{- end }}
|
||||
{{- if .OpenIDConnect }}
|
||||
openid.request.consent:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -3669,12 +3422,15 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/jose.spec.JWK'
|
||||
{{- end }}
|
||||
securitySchemes:
|
||||
authelia_auth:
|
||||
type: apiKey
|
||||
name: "{{ .Session }}"
|
||||
in: cookie
|
||||
{{- if .OpenIDConnect }}
|
||||
openid:
|
||||
type: openIdConnect
|
||||
openIdConnectUrl: "{{ .BaseURL }}.well-known/openid-configuration"
|
||||
{{- end }}
|
||||
...
|
||||
|
|
|
@ -56,6 +56,26 @@ The following functions which mimic the behaviour of helm exist in most templati
|
|||
- b64dec
|
||||
- b32enc
|
||||
- b32dec
|
||||
- list
|
||||
- dict
|
||||
- get
|
||||
- set
|
||||
- isAbs
|
||||
- base
|
||||
- dir
|
||||
- ext
|
||||
- clean
|
||||
- osBase
|
||||
- osClean
|
||||
- osDir
|
||||
- osExt
|
||||
- osIsAbs
|
||||
- deepEqual
|
||||
- typeOf
|
||||
- typeIs
|
||||
- typeIsLike
|
||||
- kindOf
|
||||
- kindIs
|
||||
|
||||
See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please
|
||||
note that only the functions listed above are supported and the functions don't necessarily behave exactly the same.
|
||||
|
|
|
@ -396,3 +396,8 @@ func (ctx *AutheliaCtx) SetContentTypeTextHTML() {
|
|||
func (ctx *AutheliaCtx) SetContentTypeApplicationJSON() {
|
||||
ctx.SetContentTypeBytes(contentTypeApplicationJSON)
|
||||
}
|
||||
|
||||
// SetContentTypeApplicationYAML efficiently sets the Content-Type header to 'application/yaml; charset=utf-8'.
|
||||
func (ctx *AutheliaCtx) SetContentTypeApplicationYAML() {
|
||||
ctx.SetContentTypeBytes(contentTypeApplicationYAML)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,62 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
func TestContentTypes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setup func(ctx *middlewares.AutheliaCtx) (err error)
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ApplicationJSON",
|
||||
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
|
||||
ctx.SetContentTypeApplicationJSON()
|
||||
|
||||
return nil
|
||||
},
|
||||
expected: "application/json; charset=utf-8",
|
||||
},
|
||||
{
|
||||
name: "ApplicationYAML",
|
||||
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
|
||||
ctx.SetContentTypeApplicationYAML()
|
||||
|
||||
return nil
|
||||
},
|
||||
expected: "application/yaml; charset=utf-8",
|
||||
},
|
||||
{
|
||||
name: "TextPlain",
|
||||
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
|
||||
ctx.SetContentTypeTextPlain()
|
||||
|
||||
return nil
|
||||
},
|
||||
expected: "text/plain; charset=utf-8",
|
||||
},
|
||||
{
|
||||
name: "TextHTML",
|
||||
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
|
||||
ctx.SetContentTypeTextHTML()
|
||||
|
||||
return nil
|
||||
},
|
||||
expected: "text/html; charset=utf-8",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
assert.NoError(t, tc.setup(mock.Ctx))
|
||||
|
||||
assert.Equal(t, tc.expected, string(mock.Ctx.Response.Header.ContentType()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuerURL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
@ -44,8 +100,8 @@ func TestIssuerURL(t *testing.T) {
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", tc.proto)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", tc.host)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, tc.proto)
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, tc.host)
|
||||
|
||||
if tc.base != "" {
|
||||
mock.Ctx.SetUserValue("base_url", tc.base)
|
||||
|
@ -103,8 +159,8 @@ func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
|
|||
func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com")
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "home.example.com")
|
||||
originalURL, err := mock.Ctx.GetOriginalURL()
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -142,7 +198,7 @@ func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) {
|
|||
mock.Ctx.RequestCtx.Request.SetHost("localhost")
|
||||
mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234")
|
||||
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-URI", "/base/2fa/one-time-password")
|
||||
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||
mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
|
||||
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-Method", "GET")
|
||||
|
||||
assert.Equal(t, []byte("https"), mock.Ctx.XForwardedProto())
|
||||
|
|
|
@ -88,6 +88,7 @@ var (
|
|||
contentTypeTextPlain = []byte("text/plain; charset=utf-8")
|
||||
contentTypeTextHTML = []byte("text/html; charset=utf-8")
|
||||
contentTypeApplicationJSON = []byte("application/json; charset=utf-8")
|
||||
contentTypeApplicationYAML = []byte("application/yaml; charset=utf-8")
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -6,14 +6,12 @@ import (
|
|||
|
||||
const (
|
||||
assetsRoot = "public_html"
|
||||
assetsSwagger = assetsRoot + "/api"
|
||||
|
||||
fileOpenAPI = "openapi.yml"
|
||||
fileIndexHTML = "index.html"
|
||||
fileLogo = "logo.png"
|
||||
|
||||
extHTML = ".html"
|
||||
extJSON = ".json"
|
||||
extYML = ".yml"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -52,8 +50,8 @@ var (
|
|||
const (
|
||||
environment = "ENVIRONMENT"
|
||||
dev = "dev"
|
||||
f = "false"
|
||||
t = "true"
|
||||
strFalse = "false"
|
||||
strTrue = "true"
|
||||
localhost = "localhost"
|
||||
schemeHTTP = "http"
|
||||
schemeHTTPS = "https"
|
||||
|
@ -76,7 +74,8 @@ X_AUTHELIA_HEALTHCHECK_PATH=%s
|
|||
`
|
||||
|
||||
const (
|
||||
tmplCSPSwagger = "default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'; base-uri 'self'"
|
||||
tmplCSPSwaggerNonce = "default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'; base-uri 'self'"
|
||||
tmplCSPSwagger = "default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self'; base-uri 'self'"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -93,9 +93,9 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
|||
func handleRouter(config schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
|
||||
optsTemplatedFile := NewTemplatedFileOptions(&config)
|
||||
|
||||
serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, optsTemplatedFile)
|
||||
serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, optsTemplatedFile)
|
||||
serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, optsTemplatedFile)
|
||||
serveIndexHandler := ServeTemplatedFile(providers.Templates.GetAssetIndexTemplate(), optsTemplatedFile)
|
||||
serveOpenAPIHandler := ServeTemplatedOpenAPI(providers.Templates.GetAssetOpenAPIIndexTemplate(), optsTemplatedFile)
|
||||
serveOpenAPISpecHandler := ETagRootURL(ServeTemplatedOpenAPI(providers.Templates.GetAssetOpenAPISpecTemplate(), optsTemplatedFile))
|
||||
|
||||
handlerPublicHTML := newPublicHTMLEmbeddedHandler()
|
||||
handlerLocales := newLocalesEmbeddedHandler()
|
||||
|
@ -126,10 +126,12 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
r.GET("/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json", middlewares.AssetOverride(config.Server.AssetPath, 0, handlerLocales))
|
||||
|
||||
// Swagger.
|
||||
r.GET("/api/", middleware(serveSwaggerHandler))
|
||||
r.GET("/api/", middleware(serveOpenAPIHandler))
|
||||
r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS)
|
||||
r.GET("/api/"+fileOpenAPI, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler)))
|
||||
r.OPTIONS("/api/"+fileOpenAPI, policyCORSPublicGET.HandleOPTIONS)
|
||||
r.GET("/api/index.html", middleware(serveOpenAPIHandler))
|
||||
r.OPTIONS("/api/index.html", policyCORSPublicGET.HandleOPTIONS)
|
||||
r.GET("/api/openapi.yml", policyCORSPublicGET.Middleware(middleware(serveOpenAPISpecHandler)))
|
||||
r.OPTIONS("/api/openapi.yml", policyCORSPublicGET.HandleOPTIONS)
|
||||
|
||||
for _, file := range filesSwagger {
|
||||
r.GET("/api/"+file, handlerPublicHTML)
|
||||
|
|
|
@ -19,6 +19,10 @@ import (
|
|||
|
||||
// CreateDefaultServer Create Authelia's internal webserver with the given configuration and providers.
|
||||
func CreateDefaultServer(config schema.Configuration, providers middlewares.Providers) (server *fasthttp.Server, listener net.Listener, err error) {
|
||||
if err = providers.Templates.LoadTemplatedAssets(assets); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load templated assets")
|
||||
}
|
||||
|
||||
server = &fasthttp.Server{
|
||||
ErrorHandler: handleError(),
|
||||
Handler: handleRouter(config, providers),
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
|
@ -134,10 +135,17 @@ type TLSServerContext struct {
|
|||
port int
|
||||
}
|
||||
|
||||
func NewTLSServerContext(configuration schema.Configuration) (*TLSServerContext, error) {
|
||||
serverContext := new(TLSServerContext)
|
||||
func NewTLSServerContext(configuration schema.Configuration) (serverContext *TLSServerContext, err error) {
|
||||
serverContext = new(TLSServerContext)
|
||||
|
||||
s, listener, err := CreateDefaultServer(configuration, middlewares.Providers{})
|
||||
providers := middlewares.Providers{}
|
||||
|
||||
providers.Templates, err = templates.New(templates.Config{EmailTemplatesPath: configuration.Notifier.TemplatePath})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, listener, err := CreateDefaultServer(configuration, providers)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1,56 +1,43 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// ServeTemplatedFile serves a templated version of a specified file,
|
||||
// this is utilised to pass information between the backend and frontend
|
||||
// and generate a nonce to support a restrictive CSP while using material-ui.
|
||||
func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) middlewares.RequestHandler {
|
||||
logger := logging.Logger()
|
||||
|
||||
a, err := assets.Open(path.Join(publicDir, file))
|
||||
if err != nil {
|
||||
logger.Fatalf("Unable to open %s: %s", file, err)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(a)
|
||||
if err != nil {
|
||||
logger.Fatalf("Unable to read %s: %s", file, err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("file").Parse(string(b))
|
||||
if err != nil {
|
||||
logger.Fatalf("Unable to parse %s template: %s", file, err)
|
||||
}
|
||||
|
||||
func ServeTemplatedFile(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler {
|
||||
isDevEnvironment := os.Getenv(environment) == dev
|
||||
ext := filepath.Ext(t.Name())
|
||||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
logoOverride := f
|
||||
var err error
|
||||
|
||||
logoOverride := strFalse
|
||||
|
||||
if opts.AssetPath != "" {
|
||||
if _, err = os.Stat(filepath.Join(opts.AssetPath, fileLogo)); err == nil {
|
||||
logoOverride = t
|
||||
logoOverride = strTrue
|
||||
}
|
||||
}
|
||||
|
||||
switch extension := filepath.Ext(file); extension {
|
||||
switch ext {
|
||||
case extHTML:
|
||||
ctx.SetContentTypeTextHTML()
|
||||
case extJSON:
|
||||
|
@ -62,8 +49,6 @@ func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) midd
|
|||
nonce := utils.RandomString(32, utils.CharSetAlphaNumeric)
|
||||
|
||||
switch {
|
||||
case publicDir == assetsSwagger:
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwagger, nonce, nonce))
|
||||
case ctx.Configuration.Server.Headers.CSPTemplate != "":
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, placeholderCSPNonce, nonce))
|
||||
case isDevEnvironment:
|
||||
|
@ -72,15 +57,99 @@ func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) midd
|
|||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce))
|
||||
}
|
||||
|
||||
if err = tmpl.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil {
|
||||
if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil {
|
||||
ctx.RequestCtx.Error("an error occurred", 503)
|
||||
logger.Errorf("Unable to execute template: %v", err)
|
||||
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ServeTemplatedOpenAPI serves templated OpenAPI related files.
|
||||
func ServeTemplatedOpenAPI(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler {
|
||||
ext := filepath.Ext(t.Name())
|
||||
|
||||
spec := ext == extYML
|
||||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
var nonce string
|
||||
|
||||
if spec {
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, tmplCSPSwagger)
|
||||
} else {
|
||||
nonce = utils.RandomString(32, utils.CharSetAlphaNumeric)
|
||||
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwaggerNonce, nonce, nonce))
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case extHTML:
|
||||
ctx.SetContentTypeTextHTML()
|
||||
case extYML:
|
||||
ctx.SetContentTypeApplicationYAML()
|
||||
default:
|
||||
ctx.SetContentTypeTextPlain()
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if err = t.Execute(ctx.Response.BodyWriter(), opts.OpenAPIData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce)); err != nil {
|
||||
ctx.RequestCtx.Error("an error occurred", 503)
|
||||
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ETagRootURL dynamically matches the If-None-Match header and adds the ETag header.
|
||||
func ETagRootURL(next middlewares.RequestHandler) middlewares.RequestHandler {
|
||||
etags := map[string][]byte{}
|
||||
|
||||
h := sha1.New() //nolint:gosec // Usage is for collision avoidance not security.
|
||||
mu := &sync.Mutex{}
|
||||
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
k := ctx.RootURLSlash().String()
|
||||
|
||||
mu.Lock()
|
||||
|
||||
etag, ok := etags[k]
|
||||
|
||||
mu.Unlock()
|
||||
|
||||
if ok && bytes.Equal(etag, ctx.Request.Header.PeekBytes(headerIfNoneMatch)) {
|
||||
ctx.Response.Header.SetBytesKV(headerETag, etag)
|
||||
ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets)
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusNotModified)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next(ctx)
|
||||
|
||||
mu.Lock()
|
||||
|
||||
h.Write(ctx.Response.Body())
|
||||
sum := h.Sum(nil)
|
||||
h.Reset()
|
||||
|
||||
etagNew := make([]byte, hex.EncodedLen(len(sum)))
|
||||
|
||||
hex.Encode(etagNew, sum)
|
||||
|
||||
if !ok || !bytes.Equal(etag, etagNew) {
|
||||
etags[k] = etagNew
|
||||
}
|
||||
|
||||
mu.Unlock()
|
||||
|
||||
ctx.Response.Header.SetBytesKV(headerETag, etagNew)
|
||||
ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets)
|
||||
}
|
||||
}
|
||||
|
||||
func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (err error) {
|
||||
if disabled {
|
||||
return nil
|
||||
|
@ -120,11 +189,17 @@ func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (er
|
|||
func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileOptions) {
|
||||
opts = &TemplatedFileOptions{
|
||||
AssetPath: config.Server.AssetPath,
|
||||
DuoSelfEnrollment: f,
|
||||
DuoSelfEnrollment: strFalse,
|
||||
RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled),
|
||||
ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable),
|
||||
ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(),
|
||||
Theme: config.Theme,
|
||||
|
||||
EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""),
|
||||
EndpointsWebauthn: !config.Webauthn.Disable,
|
||||
EndpointsTOTP: !config.TOTP.Disable,
|
||||
EndpointsDuo: !config.DuoAPI.Disable,
|
||||
EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil),
|
||||
}
|
||||
|
||||
if !config.DuoAPI.Disable {
|
||||
|
@ -143,6 +218,12 @@ type TemplatedFileOptions struct {
|
|||
ResetPasswordCustomURL string
|
||||
Session string
|
||||
Theme string
|
||||
|
||||
EndpointsPasswordReset bool
|
||||
EndpointsWebauthn bool
|
||||
EndpointsTOTP bool
|
||||
EndpointsDuo bool
|
||||
EndpointsOpenIDConnect bool
|
||||
}
|
||||
|
||||
// CommonData returns a TemplatedFileCommonData with the dynamic options.
|
||||
|
@ -161,6 +242,22 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri
|
|||
}
|
||||
}
|
||||
|
||||
// OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options.
|
||||
func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData {
|
||||
return TemplatedFileOpenAPIData{
|
||||
Base: base,
|
||||
BaseURL: baseURL,
|
||||
CSPNonce: nonce,
|
||||
|
||||
Session: options.Session,
|
||||
PasswordReset: options.EndpointsPasswordReset,
|
||||
Webauthn: options.EndpointsWebauthn,
|
||||
TOTP: options.EndpointsTOTP,
|
||||
Duo: options.EndpointsDuo,
|
||||
OpenIDConnect: options.EndpointsOpenIDConnect,
|
||||
}
|
||||
}
|
||||
|
||||
// TemplatedFileCommonData is a struct which is used for many templated files.
|
||||
type TemplatedFileCommonData struct {
|
||||
Base string
|
||||
|
@ -174,3 +271,16 @@ type TemplatedFileCommonData struct {
|
|||
Session string
|
||||
Theme string
|
||||
}
|
||||
|
||||
// TemplatedFileOpenAPIData is a struct which is used for the OpenAPI spec file.
|
||||
type TemplatedFileOpenAPIData struct {
|
||||
Base string
|
||||
BaseURL string
|
||||
CSPNonce string
|
||||
Session string
|
||||
PasswordReset bool
|
||||
Webauthn bool
|
||||
TOTP bool
|
||||
Duo bool
|
||||
OpenIDConnect bool
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ const (
|
|||
// Template File Names.
|
||||
const (
|
||||
TemplateNameEmailIdentityVerification = "IdentityVerification"
|
||||
TemplateNameEmailPasswordReset = "PasswordReset"
|
||||
TemplateNameEmailEvent = "Event"
|
||||
)
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -49,9 +51,97 @@ func FuncMap() map[string]any {
|
|||
"b64dec": FuncB64Dec,
|
||||
"b32enc": FuncB32Enc,
|
||||
"b32dec": FuncB32Dec,
|
||||
"list": FuncList,
|
||||
"dict": FuncDict,
|
||||
"get": FuncGet,
|
||||
"set": FuncSet,
|
||||
"isAbs": path.IsAbs,
|
||||
"base": path.Base,
|
||||
"dir": path.Dir,
|
||||
"ext": path.Ext,
|
||||
"clean": path.Clean,
|
||||
"osBase": filepath.Base,
|
||||
"osClean": filepath.Clean,
|
||||
"osDir": filepath.Dir,
|
||||
"osExt": filepath.Ext,
|
||||
"osIsAbs": filepath.IsAbs,
|
||||
"deepEqual": reflect.DeepEqual,
|
||||
"typeOf": FuncTypeOf,
|
||||
"typeIs": FuncTypeIs,
|
||||
"typeIsLike": FuncTypeIsLike,
|
||||
"kindOf": FuncKindOf,
|
||||
"kindIs": FuncKindIs,
|
||||
}
|
||||
}
|
||||
|
||||
// FuncTypeIs is a helper function that provides similar functionality to the helm typeIs func.
|
||||
func FuncTypeIs(is string, v any) bool {
|
||||
return is == FuncTypeOf(v)
|
||||
}
|
||||
|
||||
// FuncTypeIsLike is a helper function that provides similar functionality to the helm typeIsLike func.
|
||||
func FuncTypeIsLike(is string, v any) bool {
|
||||
t := FuncTypeOf(v)
|
||||
|
||||
return is == t || "*"+is == t
|
||||
}
|
||||
|
||||
// FuncTypeOf is a helper function that provides similar functionality to the helm typeOf func.
|
||||
func FuncTypeOf(v any) string {
|
||||
return reflect.ValueOf(v).Type().String()
|
||||
}
|
||||
|
||||
// FuncKindIs is a helper function that provides similar functionality to the helm kindIs func.
|
||||
func FuncKindIs(is string, v any) bool {
|
||||
return is == FuncKindOf(v)
|
||||
}
|
||||
|
||||
// FuncKindOf is a helper function that provides similar functionality to the helm kindOf func.
|
||||
func FuncKindOf(v any) string {
|
||||
return reflect.ValueOf(v).Kind().String()
|
||||
}
|
||||
|
||||
// FuncList is a helper function that provides similar functionality to the helm list func.
|
||||
func FuncList(items ...any) []any {
|
||||
return items
|
||||
}
|
||||
|
||||
// FuncDict is a helper function that provides similar functionality to the helm dict func.
|
||||
func FuncDict(pairs ...any) map[string]any {
|
||||
m := map[string]any{}
|
||||
p := len(pairs)
|
||||
|
||||
for i := 0; i < p; i += 2 {
|
||||
key := strval(pairs[i])
|
||||
|
||||
if i+1 >= p {
|
||||
m[key] = ""
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
m[key] = pairs[i+1]
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// FuncGet is a helper function that provides similar functionality to the helm get func.
|
||||
func FuncGet(m map[string]any, key string) any {
|
||||
if val, ok := m[key]; ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// FuncSet is a helper function that provides similar functionality to the helm set func.
|
||||
func FuncSet(m map[string]any, key string, value any) map[string]any {
|
||||
m[key] = value
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// FuncB64Enc is a helper function that provides similar functionality to the helm b64enc func.
|
||||
func FuncB64Enc(input string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(input))
|
||||
|
@ -202,7 +292,7 @@ func FuncStringQuote(in ...any) string {
|
|||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func strval(v interface{}) string {
|
||||
func strval(v any) string {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
|
@ -219,7 +309,7 @@ func strslice(v any) []string {
|
|||
switch v := v.(type) {
|
||||
case []string:
|
||||
return v
|
||||
case []interface{}:
|
||||
case []any:
|
||||
b := make([]string, 0, len(v))
|
||||
|
||||
for _, s := range v {
|
||||
|
|
|
@ -469,3 +469,97 @@ func TestFuncStringSQuote(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncTypeOf(t *testing.T) {
|
||||
astring := "typeOfExample"
|
||||
anint := 5
|
||||
astringslice := []string{astring}
|
||||
anintslice := []int{anint}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
have any
|
||||
expected string
|
||||
expectedKind string
|
||||
}{
|
||||
{"String", astring, "string", "string"},
|
||||
{"StringPtr", &astring, "*string", "ptr"},
|
||||
{"StringSlice", astringslice, "[]string", "slice"},
|
||||
{"StringSlicePtr", &astringslice, "*[]string", "ptr"},
|
||||
{"Integer", anint, "int", "int"},
|
||||
{"IntegerPtr", &anint, "*int", "ptr"},
|
||||
{"IntegerSlice", anintslice, "[]int", "slice"},
|
||||
{"IntegerSlicePtr", &anintslice, "*[]int", "ptr"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, FuncTypeOf(tc.have))
|
||||
assert.Equal(t, tc.expectedKind, FuncKindOf(tc.have))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncTypeIs(t *testing.T) {
|
||||
astring := "typeIsExample"
|
||||
anint := 10
|
||||
astringslice := []string{astring}
|
||||
anintslice := []int{anint}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
is string
|
||||
have any
|
||||
expected bool
|
||||
expectedLike bool
|
||||
expectedKind bool
|
||||
}{
|
||||
{"ShouldMatchStringAsString", "string", astring, true, true, true},
|
||||
{"ShouldMatchStringPtrAsString", "string", &astring, false, true, false},
|
||||
{"ShouldNotMatchStringAsInt", "int", astring, false, false, false},
|
||||
{"ShouldNotMatchStringSliceAsStringSlice", "[]string", astringslice, true, true, false},
|
||||
{"ShouldNotMatchStringSlicePtrAsStringSlice", "[]string", &astringslice, false, true, false},
|
||||
{"ShouldNotMatchStringSlicePtrAsStringSlicePtr", "*[]string", &astringslice, true, true, false},
|
||||
{"ShouldNotMatchStringSliceAsString", "string", astringslice, false, false, false},
|
||||
{"ShouldMatchIntAsInt", "int", anint, true, true, true},
|
||||
{"ShouldMatchIntPtrAsInt", "int", &anint, false, true, false},
|
||||
{"ShouldNotMatchIntAsString", "string", anint, false, false, false},
|
||||
{"ShouldMatchIntegerSliceAsIntSlice", "[]int", anintslice, true, true, false},
|
||||
{"ShouldMatchIntegerSlicePtrAsIntSlice", "[]int", &anintslice, false, true, false},
|
||||
{"ShouldMatchIntegerSlicePtrAsIntSlicePtr", "*[]int", &anintslice, true, true, false},
|
||||
{"ShouldNotMatchIntegerSliceAsInt", "int", anintslice, false, false, false},
|
||||
{"ShouldMatchKindSlice", "slice", anintslice, false, false, true},
|
||||
{"ShouldMatchKindPtr", "ptr", &anintslice, false, false, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, FuncTypeIs(tc.is, tc.have))
|
||||
assert.Equal(t, tc.expectedLike, FuncTypeIsLike(tc.is, tc.have))
|
||||
assert.Equal(t, tc.expectedKind, FuncKindIs(tc.is, tc.have))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncList(t *testing.T) {
|
||||
assert.Equal(t, []any{"a", "b", "c"}, FuncList("a", "b", "c"))
|
||||
assert.Equal(t, []any{1, 2, 3}, FuncList(1, 2, 3))
|
||||
}
|
||||
|
||||
func TestFuncDict(t *testing.T) {
|
||||
assert.Equal(t, map[string]any{"a": 1}, FuncDict("a", 1))
|
||||
assert.Equal(t, map[string]any{"a": 1, "b": ""}, FuncDict("a", 1, "b"))
|
||||
assert.Equal(t, map[string]any{"1": 1, "b": 2}, FuncDict(1, 1, "b", 2))
|
||||
assert.Equal(t, map[string]any{"true": 1, "b": 2}, FuncDict(true, 1, "b", 2))
|
||||
assert.Equal(t, map[string]any{"a": 2, "b": 3}, FuncDict("a", 1, "a", 2, "b", 3))
|
||||
}
|
||||
|
||||
func TestFuncGet(t *testing.T) {
|
||||
assert.Equal(t, 123, FuncGet(map[string]any{"abc": 123}, "abc"))
|
||||
assert.Equal(t, "", FuncGet(map[string]any{"abc": 123}, "123"))
|
||||
}
|
||||
|
||||
func TestFuncSet(t *testing.T) {
|
||||
assert.Equal(t, map[string]any{"abc": 123, "123": true}, FuncSet(map[string]any{"abc": 123}, "123", true))
|
||||
assert.Equal(t, map[string]any{"abc": true}, FuncSet(map[string]any{"abc": 123}, "abc", true))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// New creates a new templates' provider.
|
||||
|
@ -23,6 +25,64 @@ type Provider struct {
|
|||
templates Templates
|
||||
}
|
||||
|
||||
// LoadTemplatedAssets takes an embed.FS and loads each templated asset document into a Template.
|
||||
func (p *Provider) LoadTemplatedAssets(fs embed.FS) (err error) {
|
||||
var (
|
||||
data []byte
|
||||
)
|
||||
|
||||
if data, err = fs.ReadFile("public_html/index.html"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.templates.asset.index, err = template.
|
||||
New("assets/public_html/index.html").
|
||||
Funcs(FuncMap()).
|
||||
Parse(string(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data, err = fs.ReadFile("public_html/api/index.html"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.templates.asset.api.index, err = template.
|
||||
New("assets/public_html/api/index.html").
|
||||
Funcs(FuncMap()).
|
||||
Parse(string(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data, err = fs.ReadFile("public_html/api/openapi.yml"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.templates.asset.api.spec, err = template.
|
||||
New("api/public_html/openapi.yaml").
|
||||
Funcs(FuncMap()).
|
||||
Parse(string(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssetIndexTemplate returns a Template used to generate the React index document.
|
||||
func (p *Provider) GetAssetIndexTemplate() (t Template) {
|
||||
return p.templates.asset.index
|
||||
}
|
||||
|
||||
// GetAssetOpenAPIIndexTemplate returns a Template used to generate the OpenAPI index document.
|
||||
func (p *Provider) GetAssetOpenAPIIndexTemplate() (t Template) {
|
||||
return p.templates.asset.api.index
|
||||
}
|
||||
|
||||
// GetAssetOpenAPISpecTemplate returns a Template used to generate the OpenAPI specification document.
|
||||
func (p *Provider) GetAssetOpenAPISpecTemplate() (t Template) {
|
||||
return p.templates.asset.api.spec
|
||||
}
|
||||
|
||||
// GetEventEmailTemplate returns an EmailTemplate used for generic event notifications.
|
||||
func (p *Provider) GetEventEmailTemplate() (t *EmailTemplate) {
|
||||
return p.templates.notification.event
|
||||
}
|
||||
|
|
|
@ -9,6 +9,17 @@ import (
|
|||
// Templates is the struct which holds all the *template.Template values.
|
||||
type Templates struct {
|
||||
notification NotificationTemplates
|
||||
asset AssetTemplates
|
||||
}
|
||||
|
||||
type AssetTemplates struct {
|
||||
index *tt.Template
|
||||
api APIAssetTemplates
|
||||
}
|
||||
|
||||
type APIAssetTemplates struct {
|
||||
index *tt.Template
|
||||
spec *tt.Template
|
||||
}
|
||||
|
||||
// NotificationTemplates are the templates for the notification system.
|
||||
|
|
|
@ -39,24 +39,17 @@ func isSecretEnvKey(key string) (isSecretEnvKey bool) {
|
|||
return false
|
||||
}
|
||||
|
||||
func templateExists(path string) (exists bool) {
|
||||
func fileExists(path string) (exists bool) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func readTemplate(name, ext, category, overridePath string) (tPath string, embed bool, data []byte, err error) {
|
||||
if overridePath != "" {
|
||||
tPath = filepath.Join(overridePath, name+ext)
|
||||
|
||||
if templateExists(tPath) {
|
||||
if fileExists(tPath) {
|
||||
if data, err = os.ReadFile(tPath); err != nil {
|
||||
return tPath, false, nil, fmt.Errorf("failed to read template override at path '%s': %w", tPath, err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue