diff options
| -rw-r--r-- | .yamllint.yml | 1 | ||||
| -rw-r--r-- | api/index.html | 16 | ||||
| -rw-r--r-- | api/openapi.yml | 446 | ||||
| -rw-r--r-- | docs/content/en/reference/guides/templating.md | 20 | ||||
| -rw-r--r-- | internal/middlewares/authelia_context.go | 5 | ||||
| -rw-r--r-- | internal/middlewares/authelia_context_test.go | 66 | ||||
| -rw-r--r-- | internal/middlewares/const.go | 1 | ||||
| -rw-r--r-- | internal/server/const.go | 15 | ||||
| -rw-r--r-- | internal/server/handlers.go | 14 | ||||
| -rw-r--r-- | internal/server/server.go | 4 | ||||
| -rw-r--r-- | internal/server/server_test.go | 14 | ||||
| -rw-r--r-- | internal/server/template.go | 170 | ||||
| -rw-r--r-- | internal/templates/const.go | 1 | ||||
| -rw-r--r-- | internal/templates/funcs.go | 94 | ||||
| -rw-r--r-- | internal/templates/funcs_test.go | 94 | ||||
| -rw-r--r-- | internal/templates/provider.go | 60 | ||||
| -rw-r--r-- | internal/templates/types.go | 11 | ||||
| -rw-r--r-- | internal/templates/util.go | 13 |
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) } |
