Cache buster buster (#3091)

This commit is contained in:
James Hodgkinson 2024-10-15 11:54:46 +10:00 committed by GitHub
parent 6b48054a2e
commit c8b3b6214c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 339 additions and 223 deletions

1
Cargo.lock generated
View file

@ -3249,6 +3249,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"gix", "gix",
"serde", "serde",
"sha2",
"toml", "toml",
] ]

View file

@ -20,9 +20,10 @@ test = false
doctest = false doctest = false
[dependencies] [dependencies]
serde = { workspace = true, features = ["derive"] }
toml = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sha2 = { workspace = true }
toml = { workspace = true }
[build-dependencies] [build-dependencies]
base64 = { workspace = true } base64 = { workspace = true }

View file

@ -1,5 +1,7 @@
use base64::prelude::BASE64_STANDARD;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use serde::Deserialize; use serde::Deserialize;
use sha2::Digest;
use std::env; use std::env;
// To debug why a rebuild is requested. // To debug why a rebuild is requested.
@ -83,16 +85,22 @@ pub fn apply_profile() {
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION"); println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
println!("cargo:rerun-if-env-changed=KANIDM_PKG_COMMIT_REV"); println!("cargo:rerun-if-env-changed=KANIDM_PKG_COMMIT_REV");
let version = env!("CARGO_PKG_VERSION"); let kanidm_pkg_version = match option_env!("KANIDM_PKG_COMMIT_REV") {
if let Some(commit_rev) = option_env!("KANIDM_PKG_COMMIT_REV") { Some(commit_rev) => format!("{} {}", env!("CARGO_PKG_VERSION"), commit_rev),
println!( None => env!("CARGO_PKG_VERSION").to_string(),
"cargo:rustc-env=KANIDM_PKG_VERSION={} {}",
version, commit_rev
);
} else {
println!("cargo:rustc-env=KANIDM_PKG_VERSION={}", version);
}; };
println!("cargo:rustc-env=KANIDM_PKG_VERSION={}", kanidm_pkg_version);
// KANIDM_PKG_VERSION_HASH is used for cache busting in the web UI
let mut kanidm_pkg_version_hash = sha2::Sha256::new();
kanidm_pkg_version_hash.update(kanidm_pkg_version.as_bytes());
let kanidm_pkg_version_hash = &BASE64_STANDARD.encode(kanidm_pkg_version_hash.finalize())[..8];
println!(
"cargo:rustc-env=KANIDM_PKG_VERSION_HASH={}",
kanidm_pkg_version_hash
);
let version_pre = env!("CARGO_PKG_VERSION_PRE"); let version_pre = env!("CARGO_PKG_VERSION_PRE");
if version_pre == "dev" { if version_pre == "dev" {
println!("cargo:rustc-env=KANIDM_PRE_RELEASE=1"); println!("cargo:rustc-env=KANIDM_PRE_RELEASE=1");

View file

@ -68,7 +68,7 @@ pub fn start_logging_pipeline(
); );
// this env var gets set at build time, if we can pull it, add it to the metadata // this env var gets set at build time, if we can pull it, add it to the metadata
let git_rev = match option_env!("KANIDM_KANIDM_PKG_COMMIT_REV") { let git_rev = match option_env!("KANIDM_PKG_COMMIT_REV") {
Some(rev) => format!("-{}", rev), Some(rev) => format!("-{}", rev),
None => "".to_string(), None => "".to_string(),
}; };

View file

@ -7,6 +7,8 @@
# - set up a test oauth2 rp (https://kanidm.com) # - set up a test oauth2 rp (https://kanidm.com)
# - prompt to reset testuser's creds online # - prompt to reset testuser's creds online
set -e
if [ -n "${BUILD_MODE}" ]; then if [ -n "${BUILD_MODE}" ]; then
BUILD_MODE="--${BUILD_MODE}" BUILD_MODE="--${BUILD_MODE}"
else else

View file

@ -256,8 +256,13 @@ impl ServerConfig {
let ignorable_build_fields = [ let ignorable_build_fields = [
"KANIDM_CPU_FLAGS", "KANIDM_CPU_FLAGS",
"KANIDM_CPU_FLAGS",
"KANIDM_DEFAULT_CONFIG_PATH",
"KANIDM_DEFAULT_CONFIG_PATH", "KANIDM_DEFAULT_CONFIG_PATH",
"KANIDM_DEFAULT_UNIX_SHELL_PATH", "KANIDM_DEFAULT_UNIX_SHELL_PATH",
"KANIDM_DEFAULT_UNIX_SHELL_PATH",
"KANIDM_HTMX_UI_PKG_PATH",
"KANIDM_PKG_VERSION_HASH",
"KANIDM_PKG_VERSION", "KANIDM_PKG_VERSION",
"KANIDM_PRE_RELEASE", "KANIDM_PRE_RELEASE",
"KANIDM_PROFILE_NAME", "KANIDM_PROFILE_NAME",

View file

@ -0,0 +1,11 @@
//! Used for appending cache-busting query parameters to URLs.
//!
#[allow(dead_code)] // Because it's used in templates
/// Gets the git rev from the KANIDM_PKG_COMMIT_REV variable else drops back to the version, to allow for cache-busting parameters in URLs
#[inline]
pub fn get_cache_buster_key() -> String {
option_env!("KANIDM_PKG_VERSION_HASH") // this comes from the profiles crate at build time
.unwrap_or(env!("CARGO_PKG_VERSION"))
.to_string()
}

View file

@ -43,7 +43,7 @@ pub struct JavaScriptFile {
} }
impl JavaScriptFile { impl JavaScriptFile {
/// returns a `<script>` or `<meta>` HTML tag /// returns a `<script>` or `<meta>` HTML tag, includes the hash as a query value as a cache-busting mechanism
pub fn as_tag(&self) -> String { pub fn as_tag(&self) -> String {
let filetype = match &self.filetype { let filetype = match &self.filetype {
Some(val) => { Some(val) => {

View file

@ -1,6 +1,6 @@
mod apidocs; mod apidocs;
pub(crate) mod cache_buster;
pub(crate) mod errors; pub(crate) mod errors;
mod extractors; mod extractors;
mod generic; mod generic;
mod javascript; mod javascript;
@ -243,7 +243,7 @@ pub async fn create_https_server(
"frame-ancestors 'none'; ", "frame-ancestors 'none'; ",
"img-src 'self' data:; ", "img-src 'self' data:; ",
"worker-src 'none'; ", "worker-src 'none'; ",
"script-src 'self' 'unsafe-eval'{};" "script-src 'self' 'unsafe-eval'{};",
), ),
js_checksums js_checksums
); );

View file

@ -76,12 +76,11 @@ pub(crate) async fn ui_handler_generic(
jsfiles.push(jsfile.clone().as_tag()) jsfiles.push(jsfile.clone().as_tag())
}; };
let jstags = jsfiles.join("\n"); let body: String = format!(
let body = format!(
include_str!("ui_html.html"), include_str!("ui_html.html"),
domain_info.display_name(), jstags = jsfiles.join("\n"),
jstags, cache_buster_key = crate::https::cache_buster::get_cache_buster_key(),
display_name = domain_info.display_name()
); );
let mut res = Response::new(body); let mut res = Response::new(body);

View file

@ -5,35 +5,41 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="theme-color" content="white" /> <meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>{}</title> <title>{display_name}</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/pkg/img/logo-256.png" />
<link rel="icon"
href="/pkg/img/favicon.png?v={cache_buster_key}" />
<link rel="apple-touch-icon"
href="/pkg/img/logo-256.png?v={cache_buster_key}" />
<link rel="apple-touch-icon" sizes="180x180" <link rel="apple-touch-icon" sizes="180x180"
href="/pkg/img/logo-180.png" /> href="/pkg/img/logo-180.png?v={cache_buster_key}" />
<link rel="apple-touch-icon" sizes="192x192" <link rel="apple-touch-icon" sizes="192x192"
href="/pkg/img/logo-192.png" /> href="/pkg/img/logo-192.png?v={cache_buster_key}" />
<link rel="apple-touch-icon" sizes="512x512" <link rel="apple-touch-icon" sizes="512x512"
href="/pkg/img/logo-square.svg" /> href="/pkg/img/logo-square.svg?v={cache_buster_key}" />
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" <link rel="stylesheet" href="/pkg/external/bootstrap.min.css"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" /> integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" />
<link rel="stylesheet" href="/pkg/style.css" /> <link rel="stylesheet"
href="/pkg/style.css?v={cache_buster_key}" />
{} {jstags}
</head> </head>
<body class="flex-column d-flex h-100"> <body class="flex-column d-flex h-100">
<main class="flex-shrink-0 form-signin"> <main class="flex-shrink-0 form-signin">
<center> <center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" <img
src="/pkg/img/logo-square.svg?v={cache_buster_key}"
alt="Kanidm"
class="kanidm_logo" /> class="kanidm_logo" />
<h3>Kanidm is loading, please wait... </h3> <h3>Kanidm is loading, please wait... </h3>
</center> </center>
</main> </main>
<footer class="footer mt-auto py-3 bg-light text-end"> <footer class="footer mt-auto py-3 bg-light text-end">
<div class="container"> <div class="container">
<span class="text-muted">Powered by <a href="https://kanidm.com">Kanidm</a></span> <span class="text-muted">Powered by <a
href="https://kanidm.com">Kanidm</a></span>
</div> </div>
</footer> </footer>
</body> </body>

View file

@ -641,14 +641,14 @@ pub fn cert_generate_core(config: &Configuration) {
let (tls_key_path, tls_chain_path) = match &config.tls_config { let (tls_key_path, tls_chain_path) = match &config.tls_config {
Some(tls_config) => (tls_config.key.as_path(), tls_config.chain.as_path()), Some(tls_config) => (tls_config.key.as_path(), tls_config.chain.as_path()),
None => { None => {
error!("Unable to find tls configuration"); error!("Unable to find TLS configuration");
std::process::exit(1); std::process::exit(1);
} }
}; };
if tls_key_path.exists() && tls_chain_path.exists() { if tls_key_path.exists() && tls_chain_path.exists() {
info!( info!(
"tls key and chain already exist - remove them first if you intend to regenerate these" "TLS key and chain already exist - remove them first if you intend to regenerate these"
); );
return; return;
} }

View file

@ -870,7 +870,7 @@ async fn repl_acceptor(
// Handle *reloads* // Handle *reloads*
/* /*
_ = reload.recv() => { _ = reload.recv() => {
info!("initiate tls reload"); info!("Initiating TLS reload");
continue continue
} }
*/ */

View file

@ -1,25 +1,29 @@
<main class="p-3 x-auto"> <main class="p-3 x-auto">
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))"> <div class="(( crate::https::ui::CSS_PAGE_HEADER ))">
<h2>Applications list</h2> <h2>Applications list</h2>
</div> </div>
(% if apps.is_empty() %) (% if apps.is_empty() %)
<h5>No linked applications available</h5> <h5>No linked applications available</h5>
(% else %) (% else %)
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
(% for app in apps %) (% for app in apps %)
<div class="col-md-3"> <div class="col-md-3">
<div class="card text-center"> <div class="card text-center">
(% match app %) (% match app %)
(% when AppLink::Oauth2 with { name, display_name, redirect_url, has_image } %) (% when AppLink::Oauth2 with { name, display_name, redirect_url, has_image }
%)
<a href="(( redirect_url ))" class="link-dark stretched-link mt-2"> <a href="(( redirect_url ))" class="link-dark stretched-link mt-2">
(% if has_image %) (% if has_image %)
<img src="/ui/images/oauth2/(( name ))" class="oauth2-img" alt="((display_name)) icon" id="(( name ))"> <img src="/ui/images/oauth2/(( name ))" class="oauth2-img"
alt="((display_name)) icon" id="(( name ))">
(% else %) (% else %)
<img src="/pkg/img/icon-oauth2.svg" class="oauth2-img" alt="missing-icon icon" id="(( name ))"> <img
src="/pkg/img/icon-oauth2.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
class="oauth2-img" alt="missing-icon icon" id="(( name ))">
(% endif %) (% endif %)
</a> </a>
<label for="(( name ))">(( display_name ))</label> <label for="(( name ))">(( display_name ))</label>
(% endmatch %) (% endmatch %)
</div> </div>
</div> </div>
(% endfor %) (% endfor %)

View file

@ -1,24 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8" />
<meta name="theme-color" content="white" /> <meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>(% block title %)(( title )) - Kanidm(% endblock %)</title> <title>(% block title %)(( title )) - Kanidm(% endblock %)</title>
<link rel="icon" href="/pkg/img/favicon.png" /> (% include "base_icons.html" %)
<link rel="apple-touch-icon" href="/pkg/img/logo-256.png" />
<link rel="apple-touch-icon" sizes="180x180"
href="/pkg/img/logo-180.png" />
<link rel="apple-touch-icon" sizes="192x192"
href="/pkg/img/logo-192.png" />
<link rel="apple-touch-icon" sizes="512x512"
href="/pkg/img/logo-square.svg" />
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"/> <link rel="stylesheet"
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script> href="/pkg/external/bootstrap.min.css?v=((crate::https::cache_buster::get_cache_buster_key()))"
<link rel="stylesheet" href="/pkg/style.css" /> integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" />
<script
src="/pkg/external/bootstrap.bundle.min.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script>
<link rel="stylesheet"
href="/pkg/style.css?v=((crate::https::cache_buster::get_cache_buster_key()))" />
(% block head %)(% endblock %) (% block head %)(% endblock %)
</head> </head>
@ -26,7 +24,8 @@
(% block body %)(% endblock %) (% block body %)(% endblock %)
<footer class="footer mt-auto py-3 bg-light text-end"> <footer class="footer mt-auto py-3 bg-light text-end">
<div class="container"> <div class="container">
<span class="text-muted">Powered by <a href="https://kanidm.com">Kanidm</a></span> <span class="text-muted">Powered by <a
href="https://kanidm.com">Kanidm</a></span>
</div> </div>
</footer> </footer>
</body> </body>

View file

@ -1,25 +1,26 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8" />
<meta name="theme-color" content="white" /> <meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="htmx-config" content='{ "includeIndicatorStyles": false }' />
<title>(% block title %)(( title )) - Kanidm(% endblock %)</title> <title>(% block title %)(( title )) - Kanidm(% endblock %)</title>
<link rel="icon" href="/pkg/img/favicon.png" /> (% include "base_icons.html" %)
<link rel="apple-touch-icon" href="/pkg/img/logo-256.png" />
<link rel="apple-touch-icon" sizes="180x180"
href="/pkg/img/logo-180.png" />
<link rel="apple-touch-icon" sizes="192x192"
href="/pkg/img/logo-192.png" />
<link rel="apple-touch-icon" sizes="512x512"
href="/pkg/img/logo-square.svg" />
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"/> <link rel="stylesheet"
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script> href="/pkg/external/bootstrap.min.css?v=((crate::https::cache_buster::get_cache_buster_key()))"
<script src="/pkg/external/htmx.min.1.9.12.js" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"></script> integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" />
<link rel="stylesheet" href="/pkg/style.css" /> <script
src="/pkg/external/bootstrap.bundle.min.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script>
<script
src="/pkg/external/htmx.min.1.9.12.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"></script>
<link rel="stylesheet"
href="/pkg/style.css?v=((crate::https::cache_buster::get_cache_buster_key()))" />
(% block head %)(% endblock %) (% block head %)(% endblock %)
</head> </head>
@ -28,9 +29,9 @@
(% block body %)(% endblock %) (% block body %)(% endblock %)
<footer class="footer mt-auto py-3 bg-light text-end"> <footer class="footer mt-auto py-3 bg-light text-end">
<div class="container"> <div class="container">
<span class="text-muted">Powered by <a href="https://kanidm.com">Kanidm</a></span> <span class="text-muted">Powered by <a
href="https://kanidm.com">Kanidm</a></span>
</div> </div>
</footer> </footer>
</body> </body>
</html> </html>

View file

@ -0,0 +1,10 @@
<link rel="icon"
href="/pkg/img/favicon.png?v=((crate::https::cache_buster::get_cache_buster_key()))" />
<link rel="apple-touch-icon"
href="/pkg/img/logo-256.png?v=((crate::https::cache_buster::get_cache_buster_key()))" />
<link rel="apple-touch-icon" sizes="180x180"
href="/pkg/img/logo-180.png?v=((crate::https::cache_buster::get_cache_buster_key()))" />
<link rel="apple-touch-icon" sizes="192x192"
href="/pkg/img/logo-192.png?v=((crate::https::cache_buster::get_cache_buster_key()))" />
<link rel="apple-touch-icon" sizes="512x512"
href="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))" />

View file

@ -4,47 +4,57 @@
(% block head %) (% block head %)
<!-- TODO: janky preloading them here because I assumed htmx swapped new scripts in on boosted requests, we can replace navigation to cred update with a full redirect later, and clean this up then --> <!-- TODO: janky preloading them here because I assumed htmx swapped new scripts in on boosted requests, we can replace navigation to cred update with a full redirect later, and clean this up then -->
<script src="/pkg/external/cred_update.js"></script> <script
<script src="/pkg/external/base64.js" async></script> src="/pkg/external/cred_update.js?v=((crate::https::cache_buster::get_cache_buster_key()))"></script>
<script
src="/pkg/external/base64.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
async></script>
(% endblock %) (% endblock %)
(% block body %) (% block body %)
<main class="flex-shrink-0 container form-signin" id="cred-reset-form"> <main class="flex-shrink-0 container form-signin" id="cred-reset-form">
<center> <center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/> <img
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Kanidm" class="kanidm_logo" />
<h2>Credential Reset</h2> <h2>Credential Reset</h2>
<h3>(( domain_info.display_name() ))</h3> <h3>(( domain_info.display_name() ))</h3>
</center> </center>
<form class="mb-3"> <form class="mb-3">
<div> <div>
<label for="token" class="form-label">Enter your credential reset token.</label> <label for="token" class="form-label">Enter your credential reset
token.</label>
<input <input
id="token" id="token"
name="token" name="token"
autofocus autofocus
aria-describedby="unknown-reset-token-validation-feedback" aria-describedby="unknown-reset-token-validation-feedback"
(% if wrong_code %) (% if wrong_code %)
class='form-control is-invalid' class='form-control is-invalid'
(% else %) (% else %)
class='form-control' class='form-control'
(% endif %) (% endif %)>
> (% if wrong_code %)
(% if wrong_code %) <div id="unknown-reset-token-validation-feedback"
<div id="unknown-reset-token-validation-feedback" class="invalid-feedback"> class="invalid-feedback">
<ul><li>Unknown reset token.<br>Brand-new tokens might not be synced yet, <br>wait a few minutes before trying again.</li></ul> <ul><li>Unknown reset token.<br>Brand-new tokens might not be
synced yet, <br>wait a few minutes before trying
again.</li></ul>
</div> </div>
(% endif %) (% endif %)
</div> </div>
</form> </form>
<p class="d-flex flex-row flex-wrap justify-content-between"> <p class="d-flex flex-row flex-wrap justify-content-between">
<button class="btn btn-secondary" aria-label="Return home" hx-get="/ui" hx-target="body"> <button class="btn btn-secondary" aria-label="Return home" hx-get="/ui"
hx-target="body">
Return to the home page Return to the home page
</button> </button>
<button class="btn btn-primary" <button class="btn btn-primary"
hx-get="/ui/reset" hx-get="/ui/reset"
hx-include="#token" hx-include="#token"
hx-target="#cred-reset-form" hx-select="#cred-reset-form" hx-swap="outerHTML" hx-target="#cred-reset-form" hx-select="#cred-reset-form"
type="submit"> hx-swap="outerHTML"
type="submit">
Submit Submit
</button> </button>
</p> </p>

View file

@ -1,121 +1,147 @@
<script type="module" src="/pkg/modules/cred_update.mjs" async></script> <script type="module"
<script src="/pkg/external/base64.js" async></script> src="/pkg/modules/cred_update.mjs?v=((crate::https::cache_buster::get_cache_buster_key()))"
async></script>
<script
src="/pkg/external/base64.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
async></script>
<div class="row g-3" id="credentialUpdateDynamicSection" hx-on::before-swap="stillSwapFailureResponse(event)"> <div class="row g-3" id="credentialUpdateDynamicSection"
hx-on::before-swap="stillSwapFailureResponse(event)">
<form class="needs-validation mb-5 pb-5" novalidate> <form class="needs-validation mb-5 pb-5" novalidate>
(% match ext_cred_portal %) (% match ext_cred_portal %)
(% when CUExtPortal::None %) (% when CUExtPortal::None %)
(% when CUExtPortal::Hidden %) (% when CUExtPortal::Hidden %)
<hr class="my-4" /> <hr class="my-4" />
<p>This account is externally managed. Some features may not be available.</p> <p>This account is externally managed. Some features may not be
(% when CUExtPortal::Some(url) %) available.</p>
<hr class="my-4" /> (% when CUExtPortal::Some(url) %)
<p>This account is externally managed. Some features may not be available.</p> <hr class="my-4" />
<a href="(( url ))">Visit the external account portal</a> <p>This account is externally managed. Some features may not be
available.</p>
<a href="(( url ))">Visit the external account portal</a>
(% endmatch %) (% endmatch %)
(% if warnings.len() > 0 %) (% if warnings.len() > 0 %)
<hr class="my-4" > <hr class="my-4">
(% for warning in warnings %) (% for warning in warnings %)
(% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable, CURegWarning::Unsatisfiable].contains(warning) %) (% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable,
(% if is_danger %) CURegWarning::Unsatisfiable].contains(warning) %)
<div class='alert alert-danger' role="alert"> (% if is_danger %)
(% else %) <div class='alert alert-danger' role="alert">
<div class='alert alert-warning' role="alert"> (% else %)
<div class='alert alert-warning' role="alert">
(% endif %) (% endif %)
(% match warning %) (% match warning %)
(% when CURegWarning::MfaRequired %) (% when CURegWarning::MfaRequired %)
Multi-Factor Authentication is required for your account. Either add TOTP or remove your password in favour of passkeys to submit. Multi-Factor Authentication is required for your account. Either
(% when CURegWarning::PasskeyRequired %) add TOTP or remove your password in favour of passkeys to
Passkeys are required for your account. submit.
(% when CURegWarning::AttestedPasskeyRequired %) (% when CURegWarning::PasskeyRequired %)
Attested Passkeys are required for your account. Passkeys are required for your account.
(% when CURegWarning::AttestedResidentKeyRequired %) (% when CURegWarning::AttestedPasskeyRequired %)
Attested Resident Keys are required for your account. Attested Passkeys are required for your account.
(% when CURegWarning::WebauthnAttestationUnsatisfiable %) (% when CURegWarning::AttestedResidentKeyRequired %)
A webauthn attestation policy conflict has occurred and you will not be able to save your credentials Attested Resident Keys are required for your account.
(% when CURegWarning::Unsatisfiable %) (% when CURegWarning::WebauthnAttestationUnsatisfiable %)
An account policy conflict has occurred and you will not be able to save your credentials A webauthn attestation policy conflict has occurred and you will
not be able to save your credentials
(% when CURegWarning::Unsatisfiable %)
An account policy conflict has occurred and you will not be able
to save your credentials
(% endmatch %) (% endmatch %)
(% if is_danger %) (% if is_danger %)
<br><br> <br><br>
<b>Contact support IMMEDIATELY.</b> <b>Contact support IMMEDIATELY.</b>
(% endif %) (% endif %)
</div> </div>
(% endfor %) (% endfor %)
(% endif %) (% endif %)
<!-- Attested Passkeys --> <!-- Attested Passkeys -->
(% match attested_passkeys_state %) (% match attested_passkeys_state %)
(% when CUCredState::Modifiable %) (% when CUCredState::Modifiable %)
(% include "credentials_update_attested_passkeys.html" %) (% include "credentials_update_attested_passkeys.html" %)
<button type="button" class="btn btn-primary" hx-post="/ui/reset/add_passkey" hx-vals='{"class": "Attested"}' hx-target="#credentialUpdateDynamicSection"> <button type="button" class="btn btn-primary"
Add Attested Passkey hx-post="/ui/reset/add_passkey" hx-vals='{"class": "Attested"}'
hx-target="#credentialUpdateDynamicSection">
Add Attested Passkey
</button>
(% when CUCredState::DeleteOnly %)
(% if attested_passkeys.len() > 0 %)
(% include "credentials_update_attested_passkeys.html" %)
(% endif %)
(% when CUCredState::AccessDeny %)
(% when CUCredState::PolicyDeny %)
(% endmatch %)
<!-- Passkeys -->
(% match passkeys_state %)
(% when CUCredState::Modifiable %)
(% include "credentials_update_passkeys.html" %)
<!-- Here we are modifiable so we can render the button to add passkeys -->
<div class="btn-group">
<button type="button" class="btn btn-primary"
hx-post="/ui/reset/add_passkey"
hx-vals='{"class": "Any"}'
hx-target="#credentialUpdateDynamicSection">
Add Passkey
</button> </button>
(% when CUCredState::DeleteOnly %) <button type="button"
(% if attested_passkeys.len() > 0 %) class="btn btn-primary dropdown-toggle dropdown-toggle-split"
(% include "credentials_update_attested_passkeys.html" %) data-bs-toggle="dropdown" aria-expanded="false">
(% endif %) <span class="visually-hidden">Toggle Dropdown</span>
(% when CUCredState::AccessDeny %) </button>
(% when CUCredState::PolicyDeny %) <ul class="dropdown-menu">
(% endmatch %) <li><a class="dropdown-item" hx-post="/ui/api/cancel_mfareg"
hx-swap="none">Cancel MFA Registration
<!-- Passkeys --> session</a></li>
(% match passkeys_state %) </ul>
(% when CUCredState::Modifiable %) </div>
(% include "credentials_update_passkeys.html" %)
<!-- Here we are modifiable so we can render the button to add passkeys -->
<div class="btn-group">
<button type="button" class="btn btn-primary" hx-post="/ui/reset/add_passkey"
hx-vals='{"class": "Any"}'
hx-target="#credentialUpdateDynamicSection">
Add Passkey
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" hx-post="/ui/api/cancel_mfareg" hx-swap="none">Cancel MFA Registration session</a></li>
</ul>
</div>
(% when CUCredState::DeleteOnly %) (% when CUCredState::DeleteOnly %)
(% if passkeys.len() > 0 %) (% if passkeys.len() > 0 %)
(% include "credentials_update_passkeys.html" %) (% include "credentials_update_passkeys.html" %)
(% endif %) (% endif %)
(% when CUCredState::AccessDeny %) (% when CUCredState::AccessDeny %)
(% when CUCredState::PolicyDeny %) (% when CUCredState::PolicyDeny %)
(% endmatch %) (% endmatch %)
<!-- Password, totp credentials --> <!-- Password, totp credentials -->
(% let primary_state = primary_state %) (% let primary_state = primary_state %)
(% include "credentials_update_primary.html" %) (% include "credentials_update_primary.html" %)
<div id="cred-update-commit-bar" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> <div id="cred-update-commit-bar" class="toast" role="alert"
<div class="toast-body"> aria-live="assertive" aria-atomic="true">
<span class="d-flex align-items-center"> <div class="toast-body">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-floppy2-fill" viewBox="0 0 16 16"> <span class="d-flex align-items-center">
<path d="M12 2h-2v3h2z"/> <svg xmlns="http://www.w3.org/2000/svg" width="16"
<path d="M1.5 0A1.5 1.5 0 0 0 0 1.5v13A1.5 1.5 0 0 0 1.5 16h13a1.5 1.5 0 0 0 1.5-1.5V2.914a1.5 1.5 0 0 0-.44-1.06L14.147.439A1.5 1.5 0 0 0 13.086 0zM4 6a1 1 0 0 1-1-1V1h10v4a1 1 0 0 1-1 1zM3 9h10a1 1 0 0 1 1 1v5H2v-5a1 1 0 0 1 1-1"/> height="16" fill="currentColor"
</svg> class="bi bi-floppy2-fill" viewBox="0 0 16 16">
<b class="px-1">Careful</b>- save when you're done: <path d="M12 2h-2v3h2z" />
</span> <path
<div class="mt-2 pt-2 border-top"> d="M1.5 0A1.5 1.5 0 0 0 0 1.5v13A1.5 1.5 0 0 0 1.5 16h13a1.5 1.5 0 0 0 1.5-1.5V2.914a1.5 1.5 0 0 0-.44-1.06L14.147.439A1.5 1.5 0 0 0 13.086 0zM4 6a1 1 0 0 1-1-1V1h10v4a1 1 0 0 1-1 1zM3 9h10a1 1 0 0 1 1 1v5H2v-5a1 1 0 0 1 1-1" />
<button class="btn btn-danger" hx-post="/ui/api/cu_cancel" hx-target="body">Cancel</button> </svg>
<span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" data-bs-title="Resolve the warnings at the top."> <b class="px-1">Careful</b>- save when you're done:
<button </span>
<div class="mt-2 pt-2 border-top">
<button class="btn btn-danger"
hx-post="/ui/api/cu_cancel"
hx-target="body">Cancel</button>
<span class="d-inline-block" tabindex="0"
data-bs-toggle="tooltip"
data-bs-title="Resolve the warnings at the top.">
<button
class="btn btn-success" class="btn btn-success"
type="submit" type="submit"
hx-post="/ui/api/cu_commit" hx-post="/ui/api/cu_commit"
hx-boost="false" hx-boost="false"
(% if !warnings.is_empty() %)disabled(% endif %) (% if !warnings.is_empty() %)disabled(% endif
>Save Changes</button> %)>Save Changes</button>
</span> </span>
</div>
</div> </div>
</div> </div>
</div> </form>
</form> </div>
</div>

View file

@ -6,18 +6,21 @@
(% endblock %) (% endblock %)
(% block body %) (% block body %)
<main id="main" class="flex-shrink-0 form-signin"> <main id="main" class="flex-shrink-0 form-signin">
<center> <center>
(% if domain_custom_image %) (% if domain_custom_image %)
<img src="/ui/images/domain" alt="Kanidm" class="kanidm_logo"/> <img src="/ui/images/domain"
(% else %) alt="Kanidm" class="kanidm_logo" />
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/> (% else %)
(% endif %) <img
<h3>Kanidm</h3> src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
</center> alt="Kanidm" class="kanidm_logo" />
<div id="login-form-container" class="container"> (% endif %)
(% block logincontainer %) <h3>Kanidm</h3>
(% endblock %) </center>
</div> <div id="login-form-container" class="container">
</main> (% block logincontainer %)
(% endblock %)
</div>
</main>
(% endblock %) (% endblock %)

View file

@ -5,13 +5,19 @@
(( chal|safe )) (( chal|safe ))
</script> </script>
<script src="/pkg/external/base64.js" async></script> <script
<script src="/pkg/pkhtml.js" defer></script> src="/pkg/external/base64.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
async></script>
<script
src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
defer></script>
(% if passkey %) (% if passkey %)
<button hx-disable type="button" class="btn btn-dark" id="start-passkey-button">Use Passkey</button> <button hx-disable type="button" class="btn btn-dark"
id="start-passkey-button">Use Passkey</button>
(% else %) (% else %)
<button type="button" class="btn btn-dark" id="start-seckey-button">Use Security Key</button> <button type="button" class="btn btn-dark" id="start-seckey-button">Use Security
Key</button>
(% endif %) (% endif %)
(% endblock %) (% endblock %)

View file

@ -1,22 +1,33 @@
<nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))"> <nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))">
<div class="container-fluid"> <div class="container-fluid">
<a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))" href="/ui/apps">Kanidm</a> <a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))"
href="/ui/apps">Kanidm</a>
<!-- this shows a button on mobile devices to open the menu--> <!-- this shows a button on mobile devices to open the menu-->
<button class="navbar-toggler bg-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler bg-white" type="button"
<img src="/pkg/img/logo-square.svg" alt="Toggle navigation" class="navbar-toggler-img" /> data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false"
aria-label="Toggle navigation">
<img
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Toggle navigation" class="navbar-toggler-img" />
</button> </button>
<div class="collapse navbar-collapse" id="navbarCollapse"> <div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))"> <ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))">
<li class="mb-1"> <li class="mb-1">
<a class="nav-link" href="/ui/apps" hx-target="main" hx-select="main" hx-swap="outerHTML"><span data-feather="file"></span>Apps</a> <a class="nav-link" href="/ui/apps" hx-target="main"
hx-select="main" hx-swap="outerHTML"><span
data-feather="file"></span>Apps</a>
</li> </li>
<li class="mb-1"> <li class="mb-1">
<a class="nav-link" href="/ui/profile" hx-target="main" hx-select="main" hx-swap="outerHTML"><span data-feather="file"></span>Profile</a> <a class="nav-link" href="/ui/profile" hx-target="main"
hx-select="main" hx-swap="outerHTML"><span
data-feather="file"></span>Profile</a>
</li> </li>
<li class="mb-1"> <li class="mb-1">
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#signoutModal">Sign out</a> <a class="nav-link" href="#" data-bs-toggle="modal"
data-bs-target="#signoutModal">Sign out</a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -1,8 +1,12 @@
<div class="modal" tabindex="-1" role="dialog" id="errorModal" data-backdrop="static" data-keyboard="false" data-show="true"> <div class="modal" tabindex="-1" role="dialog" id="errorModal"
data-backdrop="static" data-keyboard="false" data-show="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><img src="/pkg/img/kani-warning.svg" alt="Kani holding warning sign" /> An error occurred</h5> <h5 class="modal-title"><img
src="/pkg/img/kani-warning.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Kani holding a warning sign" /> An error
occurred</h5>
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<p>Error Code: (( error_message ))</p> <p>Error Code: (( error_message ))</p>
@ -11,7 +15,7 @@
<div class="modal-footer"> <div class="modal-footer">
<a href="(( recovery_path ))" hx-boost="(( recovery_boosted ))"> <a href="(( recovery_path ))" hx-boost="(( recovery_boosted ))">
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Continue</button> data-bs-dismiss="modal">Continue</button>
</a> </a>
</div> </div>
</div> </div>

View file

@ -6,18 +6,20 @@
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
Are you sure you'd like to log out? Are you sure you'd like to log out?
<br/> <br />
<img src="/pkg/img/kani-waving.svg" alt="Kani waving goodbye" /> <img
src="/pkg/img/kani-waving.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Kani waving goodbye" />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="/ui/logout" hx-boost="false"> <a href="/ui/logout" hx-boost="false">
<button type="button" class="btn btn-success" <button type="button" class="btn btn-success"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#signoutModal">Sign out</button> data-bs-target="#signoutModal">Sign out</button>
</a> </a>
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button> data-bs-dismiss="modal">Cancel</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,19 +1,26 @@
(% macro side_menu_item(label, href, menu_item, icon_name) %) (% macro side_menu_item(label, href, menu_item, icon_name) %)
<a hx-select="main" hx-target="main" hx-swap="outerHTML show:false" href="(( href ))" <a hx-select="main" hx-target="main" hx-swap="outerHTML show:false"
href="(( href ))"
class="list-group-item list-group-item-action d-flex (% if menu_active_item == menu_item %) active(% endif %)"> class="list-group-item list-group-item-action d-flex (% if menu_active_item == menu_item %) active(% endif %)">
<img class="me-3" src="/pkg/img/icons/(( icon_name )).svg" alt="">(( label )) <img class="me-3"
src="/pkg/img/icons/(( icon_name )).svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt>(( label ))
</a> </a>
(% endmacro %) (% endmacro %)
<main class="container-xxl pb-5"> <main class="container-xxl pb-5">
<div class="d-flex flex-sm-row flex-column"> <div class="d-flex flex-sm-row flex-column">
<div class="list-group side-menu flex-shrink-0"> <div class="list-group side-menu flex-shrink-0">
(% call side_menu_item("Profile", "/ui/profile", ProfileMenuItems::UserProfile, "person") %) (% call side_menu_item("Profile", "/ui/profile",
(% call side_menu_item("SSH Keys", "/ui/ssh_keys", ProfileMenuItems::SshKeys, "key") %) ProfileMenuItems::UserProfile, "person") %)
(% call side_menu_item("SSH Keys", "/ui/ssh_keys",
ProfileMenuItems::SshKeys, "key") %)
(% if posix_enabled %) (% if posix_enabled %)
(% call side_menu_item("UNIX Password", "/ui/update_credentials", ProfileMenuItems::UnixPassword, "building-lock") %) (% call side_menu_item("UNIX Password", "/ui/update_credentials",
ProfileMenuItems::UnixPassword, "building-lock") %)
(% endif %) (% endif %)
(% call side_menu_item("Credentials", "/ui/update_credentials", ProfileMenuItems::Credentials, "shield-lock") %) (% call side_menu_item("Credentials", "/ui/update_credentials",
ProfileMenuItems::Credentials, "shield-lock") %)
</div> </div>
<div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4"> <div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4">
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))"> <div class="(( crate::https::ui::CSS_PAGE_HEADER ))">

View file

@ -1,4 +1,4 @@
use jsonschema::JSONSchema; use jsonschema::Validator;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
@ -29,7 +29,7 @@ async fn check_that_the_swagger_api_loads(rsclient: kanidm_client::KanidmClient)
.unwrap(); .unwrap();
let instance = serde_json::json!("foo"); let instance = serde_json::json!("foo");
let compiled = JSONSchema::compile(&schema).expect("A valid schema"); let compiled = Validator::new(&schema).expect("A valid schema");
assert!(jsonschema::is_valid(&schema, &instance)); assert!(jsonschema::is_valid(&schema, &instance));
let result = compiled.validate(&instance); let result = compiled.validate(&instance);
if let Err(errors) = result { if let Err(errors) = result {