feat: customizable static assets (#2597)

* feat: customizable static assets

This change provides the means to override specific assets from the embedded Go FS with files situated on disk.

We only allow overriding the following files currently:
* favicon.ico
* logo.png

* refactor(server): make logo string a const

* refactor(suites): override favicon and use ntp3 in traefik2 suite

* test(suites): test logo override in traefik2 suite

* test(suites): test asset override fallback in traefik suite

Closes #1630.
pull/2603/head
Amir Zarrinkafsh 2021-11-15 19:37:58 +11:00 committed by GitHub
parent 417d421b9a
commit 0be883befb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 120 additions and 17 deletions

View File

@ -7,7 +7,7 @@
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
## the system certificates store.
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
# certificates_directory: /config/certificates
# certificates_directory: /config/certificates/
## The theme to display: light, dark, grey, auto.
theme: light
@ -40,6 +40,10 @@ server:
## Must be alphanumeric chars and should not contain any slashes.
path: ""
## Set the path on disk to Authelia assets.
## Useful to allow overriding of specific static assets.
# asset_path: /config/assets/
## Buffers usually should be configured to be the same value.
## Explanation at https://www.authelia.com/docs/configuration/server.html
## Read buffer size adjusts the server's max incoming request size in bytes.

View File

@ -87,6 +87,33 @@ server:
path: authelia
```
### asset_path
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: ""
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Authelia by default serves all static assets from an embedded filesystem in the Go binary.
Modifying this setting will allow you to override and serve specific assets for Authelia from a specified path.
All files that can be overridden are documented below and must be placed in the `asset_path` with a flat file structure.
Example:
```console
/config/assets/
├── favicon.ico
└── logo.png
```
|Asset |File name|
|:-----:|:---------------:|
|Favicon|favicon.ico |
|Logo |logo.png |
### read_buffer_size
<div markdown="1">
type: integer
@ -189,3 +216,8 @@ The path to the public certificate for TLS connections. Must be in DER base64/PE
The read and write buffer sizes generally should be the same. This is because when Authelia verifies
if the user is authorized to visit a URL, it also sends back nearly the same size response as the request. However
you're able to tune these individually depending on your needs.
### Asset Overrides
If replacing the Logo for your Authelia portal it is recommended to upload a transparent PNG of your desired logo.
Authelia will automatically resize the logo to an appropriate size to present in the frontend.

View File

@ -7,7 +7,7 @@
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
## the system certificates store.
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
# certificates_directory: /config/certificates
# certificates_directory: /config/certificates/
## The theme to display: light, dark, grey, auto.
theme: light
@ -40,6 +40,10 @@ server:
## Must be alphanumeric chars and should not contain any slashes.
path: ""
## Set the path on disk to Authelia assets.
## Useful to allow overriding of specific static assets.
# asset_path: /config/assets/
## Buffers usually should be configured to be the same value.
## Explanation at https://www.authelia.com/docs/configuration/server.html
## Read buffer size adjusts the server's max incoming request size in bytes.

View File

@ -5,6 +5,7 @@ type ServerConfiguration struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
Path string `koanf:"path"`
AssetPath string `koanf:"asset_path"`
ReadBufferSize int `koanf:"read_buffer_size"`
WriteBufferSize int `koanf:"write_buffer_size"`
EnablePprof bool `koanf:"enable_endpoint_pprof"`

View File

@ -147,6 +147,7 @@ var ValidKeys = []string{
"server.read_buffer_size",
"server.write_buffer_size",
"server.path",
"server.asset_path",
"server.enable_pprof",
"server.enable_expvars",
"server.disable_healthcheck",

View File

@ -0,0 +1,29 @@
package middlewares
import (
"os"
"strings"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/utils"
)
// AssetOverrideMiddleware allows overriding and serving of specific embedded assets from disk.
func AssetOverrideMiddleware(assetPath string, next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
uri := string(ctx.RequestURI())
file := uri[strings.LastIndex(uri, "/")+1:]
if assetPath != "" && utils.IsStringInSlice(file, validOverrideAssets) {
_, err := os.Stat(assetPath + file)
if err != nil {
next(ctx)
} else {
fasthttp.FSHandler(assetPath, strings.Count(uri, "/")-1)(ctx)
}
} else {
next(ctx)
}
}
}

View File

@ -29,3 +29,4 @@ const (
)
var protoHostSeparator = []byte("://")
var validOverrideAssets = []string{"favicon.ico", "logo.png"}

View File

@ -1,9 +1,14 @@
package server
const embeddedAssets = "public_html/"
const swaggerAssets = embeddedAssets + "api/"
const apiFile = "openapi.yml"
const indexFile = "index.html"
const (
embeddedAssets = "public_html/"
swaggerAssets = embeddedAssets + "api/"
apiFile = "openapi.yml"
indexFile = "index.html"
logoFile = "logo.png"
)
var rootFiles = []string{"favicon.ico", "manifest.json", "robots.txt"}
const dev = "dev"

View File

@ -32,13 +32,12 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
embeddedPath, _ := fs.Sub(assets, "public_html")
embeddedFS := fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath)))
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != ""
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
r := router.New()
r.GET("/", serveIndexHandler)
@ -48,10 +47,10 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
r.GET("/api/"+apiFile, serveSwaggerAPIHandler)
for _, f := range rootFiles {
r.GET("/"+f, embeddedFS)
r.GET("/"+f, middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS))
}
r.GET("/static/{filepath:*}", embeddedFS)
r.GET("/static/{filepath:*}", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS))
r.ANY("/api/{filepath:*}", embeddedFS)
r.GET("/api/health", autheliaMiddleware(handlers.HealthGet))

View File

@ -16,7 +16,7 @@ import (
// 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, rememberMe, resetPassword, session, theme string, https bool) fasthttp.RequestHandler {
func ServeTemplatedFile(publicDir, file, assetPath, rememberMe, resetPassword, session, theme string, https bool) fasthttp.RequestHandler {
logger := logging.Logger()
f, err := assets.Open(publicDir + file)
@ -40,6 +40,14 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the
base = baseURL.(string)
}
logoOverride := "false"
if assetPath != "" {
if _, err := os.Stat(assetPath + logoFile); err == nil {
logoOverride = "true"
}
}
var scheme = "https"
if !https {
@ -71,7 +79,7 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce))
}
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme})
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, LogoOverride, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme})
if err != nil {
ctx.Error("an error occurred", 503)
logger.Errorf("Unable to execute template: %v", err)

View File

@ -7,6 +7,7 @@ jwt_secret: unsecure_secret
server:
port: 9091
asset_path: /config/assets/
tls:
certificate: /config/ssl/cert.pem
key: /config/ssl/key.pem

View File

@ -7,6 +7,7 @@ jwt_secret: unsecure_secret
server:
port: 9091
asset_path: /config/assets/
tls:
certificate: /config/ssl/cert.pem
key: /config/ssl/key.pem
@ -46,6 +47,9 @@ access_control:
- domain: "singlefactor.example.com"
policy: one_factor
ntp:
version: 3
notifier:
smtp:
host: smtp

View File

@ -5,5 +5,7 @@ services:
volumes:
- './Traefik2/configuration.yml:/config/configuration.yml:ro'
- './Traefik2/users.yml:/config/users.yml'
- './Traefik2/favicon.ico:/config/assets/favicon.ico'
- './Traefik2/logo.png:/config/assets/logo.png'
- './common/ssl:/config/ssl:ro'
...

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,4 +1,5 @@
VITE_HMR_PORT=8080
VITE_LOGO_OVERRIDE=false
VITE_PUBLIC_URL=""
VITE_REMEMBER_ME=true
VITE_RESET_PASSWORD=true

View File

@ -1,3 +1,4 @@
VITE_LOGO_OVERRIDE={{.LogoOverride}}
VITE_PUBLIC_URL={{.Base}}
VITE_REMEMBER_ME={{.RememberMe}}
VITE_RESET_PASSWORD={{.ResetPassword}}

View File

@ -13,7 +13,7 @@
<title>Login - Authelia</title>
</head>
<body data-basepath="%VITE_PUBLIC_URL%" data-rememberme="%VITE_REMEMBER_ME%" data-resetpassword="%VITE_RESET_PASSWORD%" data-theme="%VITE_THEME%">
<body data-basepath="%VITE_PUBLIC_URL%" data-logooverride="%VITE_LOGO_OVERRIDE%" data-rememberme="%VITE_REMEMBER_ME%" data-resetpassword="%VITE_RESET_PASSWORD%" data-theme="%VITE_THEME%">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>

View File

@ -56,7 +56,7 @@
"sourcemap": true
}
],
"^.+\\.(css|svg)$": "jest-transform-stub"
"^.+\\.(css|png|svg)$": "jest-transform-stub"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$"

View File

@ -4,6 +4,7 @@ import { Grid, makeStyles, Container, Typography, Link } from "@material-ui/core
import { grey } from "@material-ui/core/colors";
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
import { getLogoOverride } from "@utils/Configuration";
export interface Props {
id?: string;
@ -14,12 +15,17 @@ export interface Props {
const LoginLayout = function (props: Props) {
const style = useStyles();
const logo = getLogoOverride() ? (
<img src="./static/media/logo.png" alt="Logo" className={style.icon} />
) : (
<UserSvg className={style.icon} />
);
return (
<Grid id={props.id} className={style.root} container spacing={0} alignItems="center" justifyContent="center">
<Container maxWidth="xs" className={style.rootContainer}>
<Grid container>
<Grid item xs={12}>
<UserSvg className={style.icon}></UserSvg>
{logo}
</Grid>
{props.title ? (
<Grid item xs={12}>

View File

@ -7,6 +7,10 @@ export function getEmbeddedVariable(variableName: string) {
return value;
}
export function getLogoOverride() {
return getEmbeddedVariable("logooverride") === "true";
}
export function getRememberMe() {
return getEmbeddedVariable("rememberme") === "true";
}