summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.yamllint.yml1
-rw-r--r--api/index.html16
-rw-r--r--api/openapi.yml446
-rw-r--r--docs/content/en/reference/guides/templating.md20
-rw-r--r--internal/middlewares/authelia_context.go5
-rw-r--r--internal/middlewares/authelia_context_test.go66
-rw-r--r--internal/middlewares/const.go1
-rw-r--r--internal/server/const.go15
-rw-r--r--internal/server/handlers.go14
-rw-r--r--internal/server/server.go4
-rw-r--r--internal/server/server_test.go14
-rw-r--r--internal/server/template.go170
-rw-r--r--internal/templates/const.go1
-rw-r--r--internal/templates/funcs.go94
-rw-r--r--internal/templates/funcs_test.go94
-rw-r--r--internal/templates/provider.go60
-rw-r--r--internal/templates/types.go11
-rw-r--r--internal/templates/util.go13
18 files changed, 627 insertions, 418 deletions
diff --git a/.yamllint.yml b/.yamllint.yml
index 1d8112fd5..66bafeb8f 100644
--- a/.yamllint.yml
+++ b/.yamllint.yml
@@ -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
diff --git a/api/index.html b/api/index.html
index e7a8dfcf0..7d41d83af 100644
--- a/api/index.html
+++ b/api/index.html
@@ -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: [
diff --git a/api/openapi.yml b/api/openapi.yml
index 33ff52d0d..b51f5d329 100644
--- a/api/openapi.yml
+++ b/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,43 +329,44 @@ paths:
description: Forbidden
security:
- authelia_auth: []
- /api/user/info/totp:
- get:
+ /api/user/info/2fa_method:
+ post:
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.
+ summary: User Configuration
+ description: The user info 2fa_method endpoint sets the users preferred second factor method.
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/handlers.UserInfo.MethodBody'
responses:
"200":
description: Successful Operation
content:
application/json:
schema:
- $ref: '#/components/schemas/handlers.UserInfoTOTP'
+ $ref: '#/components/schemas/middlewares.OkResponse'
"403":
description: Forbidden
security:
- authelia_auth: []
- /api/user/info/2fa_method:
- post:
+ {{- if .TOTP }}
+ /api/user/info/totp:
+ get:
tags:
- User Information
- summary: User Configuration
- description: The user info 2fa_method endpoint sets the users preferred second factor method.
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/handlers.UserInfo.MethodBody'
+ 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/middlewares.OkResponse'
+ $ref: '#/components/schemas/handlers.UserInfoTOTP'
"403":
description: Forbidden
security:
@@ -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
+ handlers.bodySignTOTPRequest:
type: object
properties:
- method:
+ token:
type: string
- enum:
- - "totp"
- - "webauthn"
- - "mobile_push"
- example: totp
- middlewares.ErrorResponse:
- type: object
- properties:
- status:
+ example: "123456"
+ targetURL:
type: string
- example: KO
- message:
+ example: https://secure.example.com
+ workflow:
type: string
- example: Authentication failed, please retry later.
- middlewares.IdentityVerificationFinishBody:
- required:
- - token
- type: object
- properties:
- token:
+ example: openid_connect
+ workflowID:
type: string
- example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDc5MjU1OTYsImlzcyI6IkF1dGhlbGlhIiwiYWN0aW9uIjoiUmVzZXRQYXNzd29yZCIsInVzZXJuYW1lIjoiQW1pciJ9.636yqRrUCGCe4jsMCsonleX5CYWHncYqZum-YYb6VaY
- middlewares.OkResponse:
+ 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 }}
...
diff --git a/docs/content/en/reference/guides/templating.md b/docs/content/en/reference/guides/templating.md
index c95e9329d..b20a0b37b 100644
--- a/docs/content/en/reference/guides/templating.md
+++ b/docs/content/en/reference/guides/templating.md
@@ -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.
diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go
index e30e58399..093fd7000 100644
--- a/internal/middlewares/authelia_context.go
+++ b/internal/middlewares/authelia_context.go
@@ -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)
+}
diff --git a/internal/middlewares/authelia_context_test.go b/internal/middlewares/authelia_context_test.go
index bd857c835..3149c10a7 100644
--- a/internal/middlewares/authelia_context_test.go
+++ b/internal/middlewares/authelia_context_test.go
@@ -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())
diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go
index 83f832545..ee2c49955 100644
--- a/internal/middlewares/const.go
+++ b/internal/middlewares/const.go
@@ -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 (
diff --git a/internal/server/const.go b/internal/server/const.go
index d07c355e8..d4842f152 100644
--- a/internal/server/const.go
+++ b/internal/server/const.go
@@ -5,15 +5,13 @@ import (
)
const (
- assetsRoot = "public_html"
- assetsSwagger = assetsRoot + "/api"
+ assetsRoot = "public_html"
- fileOpenAPI = "openapi.yml"
- fileIndexHTML = "index.html"
- fileLogo = "logo.png"
+ 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 (
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index d9b4fdbfc..e88621545 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -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)
diff --git a/internal/server/server.go b/internal/server/server.go
index 760cbe50e..5c1ca950f 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -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),
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index d7f62ff43..dd87678e5 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -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
diff --git a/internal/server/template.go b/internal/server/template.go
index d86841005..cecd9d2f1 100644
--- a/internal/server/template.go
+++ b/internal/server/template.go
@@ -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)
+ 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)
- logger.Errorf("Unable to execute template: %v", err)
+ 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
+}
diff --git a/internal/templates/const.go b/internal/templates/const.go
index bf7bffa71..755807c44 100644
--- a/internal/templates/const.go
+++ b/internal/templates/const.go
@@ -8,7 +8,6 @@ const (
// Template File Names.
const (
TemplateNameEmailIdentityVerification = "IdentityVerification"
- TemplateNameEmailPasswordReset = "PasswordReset"
TemplateNameEmailEvent = "Event"
)
diff --git a/internal/templates/funcs.go b/internal/templates/funcs.go
index dc4547d03..8f0f5dcac 100644
--- a/internal/templates/funcs.go
+++ b/internal/templates/funcs.go
@@ -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 {
diff --git a/internal/templates/funcs_test.go b/internal/templates/funcs_test.go
index d6907ace8..3bfd776a3 100644
--- a/internal/templates/funcs_test.go
+++ b/internal/templates/funcs_test.go
@@ -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))
+}
diff --git a/internal/templates/provider.go b/internal/templates/provider.go
index 3c122aea2..08ce6701c 100644
--- a/internal/templates/provider.go
+++ b/internal/templates/provider.go
@@ -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
}
diff --git a/internal/templates/types.go b/internal/templates/types.go
index 9dd01de5c..51c5b254c 100644
--- a/internal/templates/types.go
+++ b/internal/templates/types.go
@@ -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.
diff --git a/internal/templates/util.go b/internal/templates/util.go
index 4b91f9979..17b336ccc 100644
--- a/internal/templates/util.go
+++ b/internal/templates/util.go
@@ -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)
}