From 0be883befb5db606b0a718cd127e48f2e84f835b Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Mon, 15 Nov 2021 19:37:58 +1100 Subject: [PATCH] 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. --- config.template.yml | 6 +++- docs/configuration/server.md | 32 ++++++++++++++++++++ internal/configuration/config.template.yml | 6 +++- internal/configuration/schema/server.go | 1 + internal/configuration/validator/const.go | 1 + internal/middlewares/asset_override.go | 29 ++++++++++++++++++ internal/middlewares/const.go | 1 + internal/server/const.go | 13 +++++--- internal/server/server.go | 11 +++---- internal/server/template.go | 12 ++++++-- internal/suites/Traefik/configuration.yml | 1 + internal/suites/Traefik2/configuration.yml | 4 +++ internal/suites/Traefik2/docker-compose.yml | 2 ++ internal/suites/Traefik2/favicon.ico | Bin 0 -> 4542 bytes internal/suites/Traefik2/logo.png | Bin 0 -> 1395 bytes web/.env.development | 1 + web/.env.production | 1 + web/index.html | 2 +- web/package.json | 2 +- web/src/layouts/LoginLayout.tsx | 8 ++++- web/src/utils/Configuration.ts | 4 +++ 21 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 internal/middlewares/asset_override.go create mode 100644 internal/suites/Traefik2/favicon.ico create mode 100644 internal/suites/Traefik2/logo.png diff --git a/config.template.yml b/config.template.yml index b1d6cda6b..2fa3825e9 100644 --- a/config.template.yml +++ b/config.template.yml @@ -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. diff --git a/docs/configuration/server.md b/docs/configuration/server.md index 1ccae5f6d..ba8cc8d5d 100644 --- a/docs/configuration/server.md +++ b/docs/configuration/server.md @@ -87,6 +87,33 @@ server: path: authelia ``` +### asset_path +
+type: string +{: .label .label-config .label-purple } +default: "" +{: .label .label-config .label-blue } +required: no +{: .label .label-config .label-green } +
+ +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
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. \ No newline at end of file diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index b1d6cda6b..2fa3825e9 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -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. diff --git a/internal/configuration/schema/server.go b/internal/configuration/schema/server.go index d07165cb0..b05c448f8 100644 --- a/internal/configuration/schema/server.go +++ b/internal/configuration/schema/server.go @@ -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"` diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index b6339ca33..b56819e1c 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -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", diff --git a/internal/middlewares/asset_override.go b/internal/middlewares/asset_override.go new file mode 100644 index 000000000..649ed965b --- /dev/null +++ b/internal/middlewares/asset_override.go @@ -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) + } + } +} diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go index 1d404a708..27c157eb9 100644 --- a/internal/middlewares/const.go +++ b/internal/middlewares/const.go @@ -29,3 +29,4 @@ const ( ) var protoHostSeparator = []byte("://") +var validOverrideAssets = []string{"favicon.ico", "logo.png"} diff --git a/internal/server/const.go b/internal/server/const.go index 9251aa95e..a01473383 100644 --- a/internal/server/const.go +++ b/internal/server/const.go @@ -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" diff --git a/internal/server/server.go b/internal/server/server.go index 907ecb0ce..1f99bfef2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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)) diff --git a/internal/server/template.go b/internal/server/template.go index a6e658452..6b6a39163 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -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) diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml index 046c181dc..d76301826 100644 --- a/internal/suites/Traefik/configuration.yml +++ b/internal/suites/Traefik/configuration.yml @@ -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 diff --git a/internal/suites/Traefik2/configuration.yml b/internal/suites/Traefik2/configuration.yml index c2427fdc7..6390464aa 100644 --- a/internal/suites/Traefik2/configuration.yml +++ b/internal/suites/Traefik2/configuration.yml @@ -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 diff --git a/internal/suites/Traefik2/docker-compose.yml b/internal/suites/Traefik2/docker-compose.yml index 3d2c16e79..4cdf56953 100644 --- a/internal/suites/Traefik2/docker-compose.yml +++ b/internal/suites/Traefik2/docker-compose.yml @@ -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' ... diff --git a/internal/suites/Traefik2/favicon.ico b/internal/suites/Traefik2/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..46d3d0e2abd67abb1d92cd5b52efb0551aef43d4 GIT binary patch literal 4542 zcmcJSdr*^C7QjEOUD>Wx9zjER1vbbVl6-l6NeCe%At5B7HtYIerM7@(N})>?n6>(; z6hz$O1FOhOqylQ|s;JYhJF{%N+nJql_R*bbZFk+GYya4Xt=0#KZtkA*a8|F*`3?zsl z&>!yCX~lbRKX46?MzjJL#w%1pvQD}97nL;iw14ejdD6uk8Fn%R815V`QYld1TS&`0 zZSrde3#9}Y2r!KO)RNWOW=c1;T2e%;F=IaXg5b4$N#0Kk$(z9^ukt>L{&1~E8>hr| zD%H_eV3-63vR1LE1IL2fDW}17(xoLl!)ny`IaIklcKXg?o17dflz!D=l~%Gw<6Qq- zmZj!27o>%U#xNOgB`kE9F_(hx#^i#w9q52fROc!5=<(%^+#< z`@*Y+gv451!XJ5`{Mw_5PQ&@Ya0d2)l+l7u+-~YA9K&|DGhlfEvdFgdiN*Bb%D4Un; z4a!kla9*`KJ{=e)xVTUX%W-V2BB~o1R>>pZ!~S`PZ?F#yQyJpP%S?QM-zHG5Rz$sn3_p;C?>x6Gua9RqfHn}DEy!@RST%zE9-F)z zV>6gxcR|MIsP{KEPjX>8hH6dhPQOi{ER#nz`5409hJ6^}&Y~oj^PwRpbA)4XDBE#P zubtj99Rudc-KMkzv^fF(4nBEvI);}taa^pUU0_>7N7nNU7tfXEUF7z`mAf5nhMk&T z{w(%mANFDEObo52v@*0|O=l#k=R0<_-DwLAML-hKrd! zJ_hvw_IqrK7iM6XY|c+BLYwV|)GT1QG3`DuacyX`K^;xMs1hD|QMH)Mb!Bu^3&$W0 zKR&X|u!CbL)tyG$wv)aq`pVW(6aV-zr4<2>QSA3rQ zF^-ih7M1Y~m#~lRbcx={GfWPZYQ&wee^5t#gm+fAo&Nm+s1eZafHnI$V7NKUoizdV zLmk?)Mses%Ox&-yJ4+?{i@%numPqEUrUiAY>F_MH!-hE;7%m6i-B`l_!#%va!5>E7 zbY#I>14ZqIdJby2Cddz;2G&t<7T4h{IvR{(G1_^68|sJm_`7>@y+*|O0qSOIr(8M< zFnF58X=AP?;j7S<-UfqD1C(e z6cyIAkgGrBWAkpGO~K`$9*ax_49CDH@3mXAW?U2Bt)QM9=NT?QUozz(|HHsBhdvx) z^ACJnJm|I2&2P9F?!66TaJD5H3=-L^da-&oyV>+73YiFCILFs==#zgwWX)M|!tMU1 z{+n%$$DzmH+*hR72tIk2^GRQAe|@hdr=#7HS=LsNnT;|0IG#1z8hu4dVV6y|7yMx& zFi-ODIA1mwWTelw#^cg=!+Y%ktJDK=ak|}{^#w4DLw>l`WJ;a#PWb;mlaLU?!X!ed ztv<=-aq^*XAvrGH&`4!1r$HvtvzFVJwjSLcm!{9`R+ b1jldY+~(xOLc+<1@C$Jqex^^3`s9BAHi1lt literal 0 HcmV?d00001 diff --git a/internal/suites/Traefik2/logo.png b/internal/suites/Traefik2/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2e35e47eb5a9371fb698bbc13342deaea6947d GIT binary patch literal 1395 zcmV-(1&sQMP){GK~!jg&6-DgA`df+-> zrt&BP+koeQuOfByMl0|M_N(WCjfj!iK%QBp->Ccy?~jq8U3FxEwZMo9{u}wa=cO!X z4)8qikIFfrazZ~`MO+Mgjh!FsfOD!POwj_Y$Ijz@z(r{bS&99M1HiRu3#fp0;AiY; zS&sx#P--454uiZKYnZ6eosXo$M)cI4>|xAAY*$Rc)^jT4tSNY1>BUkfEkYR zOb;Hx!^vc^UU38NL-Fkjiu)Mof(&*JehJjO5Us^}MOWu>Yk(1FDmOdIZN_@hu1YVr zBICG-pS%Ak(1>zntB34%)wh6zb7f&ds@FZ3H!0&ZjmrbGJakyEblc0lumrnE`a~XA zr)1E?a;1k3Ic$*8W*SS^7vt_FgJkC7`G@v916g}R8*=x2DQ#5 z*secTBW2RYQLUR2Y$7xt!02Q;`jka6fabW?Gn0~s#sEeK30po~kOzJbpli_jc!H$3 zLAEX-3cM3Q=YltY(Ez$;t%m`=M2gaon+55WlO z#-u&K{e<-zSUdU?;0JwJ9HCG*VUvF{yb?)o?C48?Z}p))g5ht#hfyjC{y;KrF)()Y zf*dxOjcP+V3I|-Sd}8L>y%F+3*6b6i$EltC7s`B9B=eUEH*6>IhoQGCuRW!WtF^H^ zl6eF0EB=NJBU$cPY*K!YmNw2syxu69e~7?V{Eh34lnD~VpJs!J=m(Mf!wleo6a>`* z42;1*2R0}mnj&e)i82zUZ_ zBWD8#l=ev%g3>^EGv(6=86-bwj*!wUE=DF;&)kDYpnW0mml95Pvf6kC7%| zIIQ}5VFmUw&C}X;8M0(l+o(!Lc4_+vGUtMDC$cDx1D}@su2ne%e4xKafjdGCGw38f zAWplC0e8h3W^fy_7S-CQapZ6`Ho*TM79d`!mWK5pchIVWEbt8W^37D05#%;f)o$&T zVliRSe@NMeL{3wVrHJ=R`!+)Xxm3Dz>P#t*9CGBem+<9c6!;to<#MEe*lqn}n1$R~ zz5%!dxtDbTlD2O`wp0Zq8FTLogin - Authelia - +
diff --git a/web/package.json b/web/package.json index 7f52c6f5a..2606e0228 100644 --- a/web/package.json +++ b/web/package.json @@ -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)$" diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx index 376880ab1..a47e98793 100644 --- a/web/src/layouts/LoginLayout.tsx +++ b/web/src/layouts/LoginLayout.tsx @@ -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() ? ( + Logo + ) : ( + + ); return ( - + {logo} {props.title ? ( diff --git a/web/src/utils/Configuration.ts b/web/src/utils/Configuration.ts index a84dc308a..73aab612e 100644 --- a/web/src/utils/Configuration.ts +++ b/web/src/utils/Configuration.ts @@ -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"; }