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",
"gix",
"serde",
"sha2",
"toml",
]

View file

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

View file

@ -1,5 +1,7 @@
use base64::prelude::BASE64_STANDARD;
use base64::{engine::general_purpose, Engine as _};
use serde::Deserialize;
use sha2::Digest;
use std::env;
// 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=KANIDM_PKG_COMMIT_REV");
let version = env!("CARGO_PKG_VERSION");
if let Some(commit_rev) = option_env!("KANIDM_PKG_COMMIT_REV") {
println!(
"cargo:rustc-env=KANIDM_PKG_VERSION={} {}",
version, commit_rev
);
} else {
println!("cargo:rustc-env=KANIDM_PKG_VERSION={}", version);
let kanidm_pkg_version = match option_env!("KANIDM_PKG_COMMIT_REV") {
Some(commit_rev) => format!("{} {}", env!("CARGO_PKG_VERSION"), commit_rev),
None => env!("CARGO_PKG_VERSION").to_string(),
};
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");
if version_pre == "dev" {
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
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),
None => "".to_string(),
};

View file

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

View file

@ -256,8 +256,13 @@ impl ServerConfig {
let ignorable_build_fields = [
"KANIDM_CPU_FLAGS",
"KANIDM_CPU_FLAGS",
"KANIDM_DEFAULT_CONFIG_PATH",
"KANIDM_DEFAULT_CONFIG_PATH",
"KANIDM_DEFAULT_UNIX_SHELL_PATH",
"KANIDM_DEFAULT_UNIX_SHELL_PATH",
"KANIDM_HTMX_UI_PKG_PATH",
"KANIDM_PKG_VERSION_HASH",
"KANIDM_PKG_VERSION",
"KANIDM_PRE_RELEASE",
"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 {
/// 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 {
let filetype = match &self.filetype {
Some(val) => {

View file

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

View file

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

View file

@ -5,35 +5,41 @@
<meta charset="utf-8" />
<meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" />
<title>{}</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<title>{display_name}</title>
<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"
href="/pkg/img/logo-180.png" />
href="/pkg/img/logo-180.png?v={cache_buster_key}" />
<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"
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"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" />
<link rel="stylesheet" href="/pkg/style.css" />
<link rel="stylesheet"
href="/pkg/style.css?v={cache_buster_key}" />
{}
{jstags}
</head>
<body class="flex-column d-flex h-100">
<main class="flex-shrink-0 form-signin">
<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" />
<h3>Kanidm is loading, please wait... </h3>
</center>
</main>
<footer class="footer mt-auto py-3 bg-light text-end">
<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>
</footer>
</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 {
Some(tls_config) => (tls_config.key.as_path(), tls_config.chain.as_path()),
None => {
error!("Unable to find tls configuration");
error!("Unable to find TLS configuration");
std::process::exit(1);
}
};
if tls_key_path.exists() && tls_chain_path.exists() {
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;
}

View file

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

View file

@ -1,25 +1,29 @@
<main class="p-3 x-auto">
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))">
<h2>Applications list</h2>
</div>
</div>
(% if apps.is_empty() %)
<h5>No linked applications available</h5>
<h5>No linked applications available</h5>
(% else %)
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
(% for app in apps %)
<div class="col-md-3">
<div class="card text-center">
(% match app %)
(% when AppLink::Oauth2 with { name, display_name, redirect_url, has_image } %)
(% match app %)
(% when AppLink::Oauth2 with { name, display_name, redirect_url, has_image }
%)
<a href="(( redirect_url ))" class="link-dark stretched-link mt-2">
(% 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 %)
<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 %)
</a>
<label for="(( name ))">(( display_name ))</label>
(% endmatch %)
(% endmatch %)
</div>
</div>
(% endfor %)

View file

@ -1,24 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" />
<title>(% block title %)(( title )) - Kanidm(% endblock %)</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<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" />
(% include "base_icons.html" %)
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"/>
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script>
<link rel="stylesheet" href="/pkg/style.css" />
<link rel="stylesheet"
href="/pkg/external/bootstrap.min.css?v=((crate::https::cache_buster::get_cache_buster_key()))"
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 %)
</head>
@ -26,7 +24,8 @@
(% block body %)(% endblock %)
<footer class="footer mt-auto py-3 bg-light text-end">
<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>
</footer>
</body>

View file

@ -1,25 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" />
<meta name="htmx-config" content='{ "includeIndicatorStyles": false }' />
<title>(% block title %)(( title )) - Kanidm(% endblock %)</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<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" />
(% include "base_icons.html" %)
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"/>
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"></script>
<script src="/pkg/external/htmx.min.1.9.12.js" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2"></script>
<link rel="stylesheet" href="/pkg/style.css" />
<link rel="stylesheet"
href="/pkg/external/bootstrap.min.css?v=((crate::https::cache_buster::get_cache_buster_key()))"
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>
<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 %)
</head>
@ -28,9 +29,9 @@
(% block body %)(% endblock %)
<footer class="footer mt-auto py-3 bg-light text-end">
<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>
</footer>
</body>
</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 %)
<!-- 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 src="/pkg/external/base64.js" async></script>
<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 %)
(% block body %)
<main class="flex-shrink-0 container form-signin" id="cred-reset-form">
<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>
<h3>(( domain_info.display_name() ))</h3>
</center>
<form class="mb-3">
<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
id="token"
name="token"
autofocus
aria-describedby="unknown-reset-token-validation-feedback"
(% if wrong_code %)
class='form-control is-invalid'
(% else %)
class='form-control'
(% endif %)
>
(% if wrong_code %)
<div id="unknown-reset-token-validation-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>
id="token"
name="token"
autofocus
aria-describedby="unknown-reset-token-validation-feedback"
(% if wrong_code %)
class='form-control is-invalid'
(% else %)
class='form-control'
(% endif %)>
(% if wrong_code %)
<div id="unknown-reset-token-validation-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>
</div>
(% endif %)
(% endif %)
</div>
</form>
<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
</button>
<button class="btn btn-primary"
hx-get="/ui/reset"
hx-include="#token"
hx-target="#cred-reset-form" hx-select="#cred-reset-form" hx-swap="outerHTML"
type="submit">
hx-get="/ui/reset"
hx-include="#token"
hx-target="#cred-reset-form" hx-select="#cred-reset-form"
hx-swap="outerHTML"
type="submit">
Submit
</button>
</p>

View file

@ -1,121 +1,147 @@
<script type="module" src="/pkg/modules/cred_update.mjs" async></script>
<script src="/pkg/external/base64.js" async></script>
<script type="module"
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>
(% match ext_cred_portal %)
(% when CUExtPortal::None %)
(% when CUExtPortal::Hidden %)
<hr class="my-4" />
<p>This account is externally managed. Some features may not be available.</p>
(% when CUExtPortal::Some(url) %)
<hr class="my-4" />
<p>This account is externally managed. Some features may not be available.</p>
<a href="(( url ))">Visit the external account portal</a>
(% when CUExtPortal::None %)
(% when CUExtPortal::Hidden %)
<hr class="my-4" />
<p>This account is externally managed. Some features may not be
available.</p>
(% when CUExtPortal::Some(url) %)
<hr class="my-4" />
<p>This account is externally managed. Some features may not be
available.</p>
<a href="(( url ))">Visit the external account portal</a>
(% endmatch %)
(% if warnings.len() > 0 %)
<hr class="my-4" >
(% for warning in warnings %)
(% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable, CURegWarning::Unsatisfiable].contains(warning) %)
(% if is_danger %)
<div class='alert alert-danger' role="alert">
(% else %)
<div class='alert alert-warning' role="alert">
<hr class="my-4">
(% for warning in warnings %)
(% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable,
CURegWarning::Unsatisfiable].contains(warning) %)
(% if is_danger %)
<div class='alert alert-danger' role="alert">
(% else %)
<div class='alert alert-warning' role="alert">
(% endif %)
(% match warning %)
(% when CURegWarning::MfaRequired %)
Multi-Factor Authentication is required for your account. Either add TOTP or remove your password in favour of passkeys to submit.
(% when CURegWarning::PasskeyRequired %)
Passkeys are required for your account.
(% when CURegWarning::AttestedPasskeyRequired %)
Attested Passkeys are required for your account.
(% when CURegWarning::AttestedResidentKeyRequired %)
Attested Resident Keys are required for your account.
(% when CURegWarning::WebauthnAttestationUnsatisfiable %)
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
(% when CURegWarning::MfaRequired %)
Multi-Factor Authentication is required for your account. Either
add TOTP or remove your password in favour of passkeys to
submit.
(% when CURegWarning::PasskeyRequired %)
Passkeys are required for your account.
(% when CURegWarning::AttestedPasskeyRequired %)
Attested Passkeys are required for your account.
(% when CURegWarning::AttestedResidentKeyRequired %)
Attested Resident Keys are required for your account.
(% when CURegWarning::WebauthnAttestationUnsatisfiable %)
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 %)
(% if is_danger %)
<br><br>
<b>Contact support IMMEDIATELY.</b>
<br><br>
<b>Contact support IMMEDIATELY.</b>
(% endif %)
</div>
</div>
(% endfor %)
(% endif %)
(% endif %)
<!-- Attested Passkeys -->
(% match attested_passkeys_state %)
<!-- Attested Passkeys -->
(% match attested_passkeys_state %)
(% when CUCredState::Modifiable %)
(% 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">
Add Attested Passkey
(% 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">
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>
(% 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 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>
<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 %)
(% if passkeys.len() > 0 %)
(% include "credentials_update_passkeys.html" %)
(% endif %)
(% if passkeys.len() > 0 %)
(% include "credentials_update_passkeys.html" %)
(% endif %)
(% when CUCredState::AccessDeny %)
(% when CUCredState::PolicyDeny %)
(% endmatch %)
(% endmatch %)
<!-- Password, totp credentials -->
(% let primary_state = primary_state %)
(% include "credentials_update_primary.html" %)
<!-- Password, totp credentials -->
(% let primary_state = primary_state %)
(% include "credentials_update_primary.html" %)
<div id="cred-update-commit-bar" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body">
<span class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-floppy2-fill" viewBox="0 0 16 16">
<path d="M12 2h-2v3h2z"/>
<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"/>
</svg>
<b class="px-1">Careful</b>- save when you're done:
</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
<div id="cred-update-commit-bar" class="toast" role="alert"
aria-live="assertive" aria-atomic="true">
<div class="toast-body">
<span class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16"
height="16" fill="currentColor"
class="bi bi-floppy2-fill" viewBox="0 0 16 16">
<path d="M12 2h-2v3h2z" />
<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" />
</svg>
<b class="px-1">Careful</b>- save when you're done:
</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"
type="submit"
hx-post="/ui/api/cu_commit"
hx-boost="false"
(% if !warnings.is_empty() %)disabled(% endif %)
>Save Changes</button>
</span>
(% if !warnings.is_empty() %)disabled(% endif
%)>Save Changes</button>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
</form>
</div>

View file

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

View file

@ -5,13 +5,19 @@
(( chal|safe ))
</script>
<script src="/pkg/external/base64.js" async></script>
<script src="/pkg/pkhtml.js" defer></script>
<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 %)
<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 %)
<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 %)
(% endblock %)

View file

@ -1,22 +1,33 @@
<nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))">
<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-->
<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">
<img src="/pkg/img/logo-square.svg" alt="Toggle navigation" class="navbar-toggler-img" />
<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">
<img
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Toggle navigation" class="navbar-toggler-img" />
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))">
<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 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 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>
</ul>
</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-content">
<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 class="modal-body text-center">
<p>Error Code: (( error_message ))</p>
@ -11,7 +15,7 @@
<div class="modal-footer">
<a href="(( recovery_path ))" hx-boost="(( recovery_boosted ))">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Continue</button>
data-bs-dismiss="modal">Continue</button>
</a>
</div>
</div>

View file

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

View file

@ -1,19 +1,26 @@
(% 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 %)">
<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>
(% endmacro %)
<main class="container-xxl pb-5">
<div class="d-flex flex-sm-row flex-column">
<div class="list-group side-menu flex-shrink-0">
(% call side_menu_item("Profile", "/ui/profile", ProfileMenuItems::UserProfile, "person") %)
(% call side_menu_item("SSH Keys", "/ui/ssh_keys", ProfileMenuItems::SshKeys, "key") %)
(% call side_menu_item("Profile", "/ui/profile",
ProfileMenuItems::UserProfile, "person") %)
(% call side_menu_item("SSH Keys", "/ui/ssh_keys",
ProfileMenuItems::SshKeys, "key") %)
(% 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 %)
(% 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 id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4">
<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 tracing::info;
@ -29,7 +29,7 @@ async fn check_that_the_swagger_api_loads(rsclient: kanidm_client::KanidmClient)
.unwrap();
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));
let result = compiled.validate(&instance);
if let Err(errors) = result {