More UI things (#911)

* Instead of wasm_bindgen creating a JS snippet to externalize code, we're now loading pure-JS util functions from wasmloader.js (#[wasm_bindgen(raw_module = "/pkg/wasmloader.js")])
* Sign out is now a confirmation box instead of "oh no I have to log back in because I'm clumsy and clicked a thing"
* Now using the urlencoding crate for encoding the TOTP URLs because string replacing encoded characters felt like writing our own crypto (and now you can call yourself whatever arbitrary string you want)
  * This fixed an issue in the web UI where the "Add a TOTP" interface would show URL-encoded things, but also made things easier for consistency.
* Moved the other web middleware objects into the middleware module because the main module was getting a bit unwieldy.
* Started auto-generating the integrity hashes in a different way on start up, which removes a middleware doing random string replacements to inject them, and means we can update modules without having to manually update the string values in the HTML.
This commit is contained in:
James Hodgkinson 2022-07-11 16:33:18 +10:00 committed by GitHub
parent 8683d452fe
commit d8f195915d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 815 additions and 374 deletions

8
Cargo.lock generated
View file

@ -2044,6 +2044,7 @@ dependencies = [
"tracing-serde",
"tracing-subscriber",
"url",
"urlencoding",
"users",
"uuid 1.1.2",
"validator",
@ -2078,6 +2079,7 @@ dependencies = [
"serde_json",
"time 0.2.27",
"url",
"urlencoding",
"uuid 1.1.2",
"webauthn-rs",
]
@ -4169,6 +4171,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
[[package]]
name = "users"
version = "0.11.0"

View file

@ -142,7 +142,6 @@ docs/pykanidm/build:
poetry install && \
poetry run mkdocs build
docs/pykanidm/serve: ## Run the local mkdocs server
docs/pykanidm/serve:
cd pykanidm && \

168
artwork/kani-waving.svg Normal file
View file

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 135.46667 135.46666"
version="1.1"
id="svg1084"
sodipodi:docname="kani-waving.svg"
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
inkscape:export-filename="logo-180.png"
inkscape:export-xdpi="33.75"
inkscape:export-ydpi="33.75"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title332">Kanidm Square Logo</title>
<sodipodi:namedview
id="namedview39"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="true"
inkscape:zoom="1"
inkscape:cx="178"
inkscape:cy="304.5"
inkscape:window-width="1389"
inkscape:window-height="847"
inkscape:window-x="51"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg1084">
<sodipodi:guide
position="-52.916665,67.733332"
orientation="0,1"
id="guide145"
inkscape:locked="false"
inkscape:label="vertical_middle"
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="121.97292,175.15416"
orientation="-1,0"
id="guide777"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="13.49375,173.03749"
orientation="-1,0"
id="guide779"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="22.754166,121.97292"
orientation="0,1"
id="guide781"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="4.4979168,13.758333"
orientation="0,1"
id="guide783"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1081" />
<metadata
id="metadata330">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Kanidm Square Logo</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="matrix(1.0952772,0,0,1.0952772,-42.847275,-49.68044)"
inkscape:label="kani"
style="display:inline">
<path
id="path5404"
d="m 62.537712,110.23377 c -3.36297,8.12708 5.78522,15.61876 5.82446,15.73981 -1.67096,-4.90156 -3.43697,-8.85836 -0.189,-12.28424"
style="display:inline;fill:#803300;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="display:inline;fill:#803300;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 138.4423,109.69923 c 3.36297,8.12708 -5.78522,15.61876 -5.82446,15.7398 1.67096,-4.90155 3.43697,-8.85835 0.189,-12.28423"
id="path5408" />
<path
id="path4529"
d="m 100.24698,67.21217 c -1.308628,-0.01556 -2.560878,2.352773 -4.890668,6.986654 -7.05187,-7.755768 -7.41258,-7.849666 -9.84229,2.649966 -0.11364,0.04758 -0.22709,0.0959 -0.34055,0.144695 -8.88231,-5.324406 -9.34124,-5.366051 -8.66717,5.041551 -0.21569,0.159933 -0.43121,0.322088 -0.64596,0.486794 -9.102,-3.605578 -9.49555,-3.378386 -7.19232,6.611477 -0.10784,0.11671 -0.21599,0.23284 -0.3235,0.35089 -9.27685,-2.506375 -9.53641,-2.129764 -6.07663,7.65845 -0.14847,0.21295 -0.29578,0.42957 -0.44338,0.64544 -10.45618,-1.11775 -10.67098,-0.91353 -5.35523,8.828413 -0.026,0.0482 -0.0526,0.0943 -0.0786,0.14263 -4.28381,2.04782 -5.70197,4.54381 -7.73183,7.89409 -0.15756,6.64457 6.9544,12.37105 17.18035,18.87895 -3.51325,-6.5113 -9.00735,-9.81207 -10.0614,-17.88832 1.54367,-2.90538 4.16877,-3.90901 6.42286,-5.21622 24.61995,14.36815 50.615808,14.53471 76.550928,-0.55035 2.26015,1.31503 4.90084,2.31509 6.45076,5.23224 -1.05405,8.07624 -6.54866,11.3765 -10.06191,17.8878 10.22594,-6.5079 17.33842,-12.23387 17.18086,-18.87844 -1.97354,-3.25733 -3.37677,-5.706 -7.38973,-7.72149 5.01291,-9.218263 4.53571,-9.305023 -5.98567,-8.177293 -0.0891,-0.1152 -0.17795,-0.23186 -0.26717,-0.34623 3.63467,-10.261532 3.40665,-10.529692 -6.39186,-7.87136 2.47993,-10.705895 2.14854,-10.749554 -7.77938,-6.803718 0.74781,-11.285018 0.47993,-11.261581 -8.88835,-5.642548 -2.49815,-10.818768 -2.79293,-10.786576 -9.94048,-2.924887 -2.68846,-4.933638 -4.10296,-7.403383 -5.43173,-7.419184 z"
style="display:inline;fill:#ff6600;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path5389"
d="m 75.334492,106.8255 c -0.51041,0.004 -0.85068,0.0544 -0.79478,0.0889 0,0 -9.13406,11.93611 0.31006,18.65881 a 9.7471106,8.2770874 37.793943 0 0 1.43712,9.42372 9.7471106,8.2770874 37.793943 0 0 10.56473,3.9672 l -3.56412,-8.60569 9.19685,0.73536 a 9.7471106,8.2770874 37.793943 0 0 -7.02645,-8.91057 9.7471106,8.2770874 37.793943 0 0 -7.00784,0.009 c -1.86231,-1.36401 -4.6287,-4.92596 -0.0408,-12.47521 1.51807,-2.49795 -1.54349,-2.90314 -3.07475,-2.89129 z"
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="path5389" />
<g
id="g5758"
transform="translate(-2.3193819,-7.5517449)"
style="display:inline"
inkscape:label="kani">
<path
style="display:inline;fill:#d45500;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 98.808992,121.55238 c -0.682767,-0.0559 4.468828,10.17565 8.051908,0.17277 0.24133,-0.67372 -3.69223,2.90976 -8.051908,-0.17277 z"
id="path5399" />
<g
id="g351">
<path
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 93.212672,111.5632 c -5.131386,10.99206 5.618332,14.73472 4.412511,4.11951 -0.319289,-3.2881 -2.328089,-7.17448 -4.412511,-4.11951 z"
id="path5415" />
<path
style="fill:#ffffff;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 95.394393,112.41429 c -1.183441,1.51896 -0.08172,3.83605 -0.09872,3.98526 1.674403,1.64132 1.61267,-4.77034 0.09872,-3.98526 z"
id="path5417" />
</g>
<g
id="g355">
<path
id="path5433"
d="m 109.6169,111.5632 c -5.13141,10.99206 5.61836,14.73472 4.41254,4.11951 -0.31929,-3.2881 -2.3281,-7.17448 -4.41254,-4.11951 z"
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="path5435"
d="m 111.79864,112.41429 c -1.18345,1.51896 -0.0817,3.83605 -0.0987,3.98526 1.67441,1.64132 1.61267,-4.77034 0.0987,-3.98526 z"
style="display:inline;fill:#ffffff;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</g>
<path
id="path5732"
d="m 111.51646,83.878113 c -0.0644,2.604234 0.83829,5.245703 2.49515,7.301842 2.19739,2.716867 5.40747,4.034785 8.37885,3.439994 6.01625,8.488971 -0.30315,11.998681 -0.4866,12.038481 -1.32922,0.42046 0.0534,6.32365 4.60901,2.5271 7.11608,-5.93031 2.73673,-13.62732 0.0641,-17.143859 1.54476,-1.912559 2.20351,-4.57778 1.80861,-7.317326 -0.6031,-4.141273 -3.48425,-7.674191 -7.13223,-8.745669 l -1.25194,9.229861 -1.06142,-0.736985 z"
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="arm-up"
transform="matrix(1.0952772,0,0,1.0952772,-42.847275,-49.680439)">
<set id="show_frame0" attributeName="visibility" attributeType="CSS" to="visible"
begin="0ms; hide_frame1.end" dur="500ms" fill="freeze"/>
<set id="hide_frame0" attributeName="visibility" attributeType="CSS" to="hidden"
begin="show_frame0.end" dur="1ms" fill="freeze"/>
</path>
<path
id="path294"
d="m 136.84733,83.996879 c -2.18137,1.42401 -3.84498,3.66555 -4.59874,6.19631 -0.99138,3.35068 -0.25622,6.742 1.91889,8.85192 -3.57942,9.769591 -10.05414,6.555411 -10.19096,6.426891 -1.10017,-0.85629 -5.17794,3.63047 0.53268,5.22929 8.92019,2.49747 12.77572,-5.47478 14.1562,-9.67038 2.45132,0.18756 5.02004,-0.78148 7.05239,-2.660471 3.06875,-2.84546 4.34444,-7.22212 3.15795,-10.83433 l -8.31186,4.20365 0.005,-1.29218 z"
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="arm-down"
transform="matrix(1.0952772,0,0,1.0952772,-42.847275,-49.680439)" visibility="hidden">
<set id="show_frame1" attributeName="visibility" attributeType="CSS" to="visible"
begin="hide_frame0.end" dur="500ms" fill="freeze"/>
<set id="hide_frame1" attributeName="visibility" attributeType="CSS" to="hidden"
begin="show_frame1.end" dur="1ms" fill="freeze"/>
</path>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -19,4 +19,4 @@ webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"
# Can not upgrade due to breaking timezone apis.
time = { version = "=0.2.27", features = ["serde", "std"] }
url = { version = "^2.2.2", features = ["serde"] }
urlencoding = "2.1.0"

View file

@ -792,22 +792,13 @@ pub struct TotpSecret {
impl TotpSecret {
/// <https://github.com/google/google-authenticator/wiki/Key-Uri-Format>
pub fn to_uri(&self) -> String {
// label = accountname / issuer (“:” / “%3A”) *”%20” accountname
// This is already done server side but paranoia is good!
let accountname = self
.accountname
.replace(':', "")
.replace("%3A", "")
.replace(' ', "%20");
let issuer = self
.issuer
.replace(':', "")
.replace("%3A", "")
.replace(' ', "%20");
let accountname = urlencoding::Encoded(&self.accountname);
let issuer = urlencoding::Encoded(&self.issuer);
let label = format!("{}:{}", issuer, accountname);
let algo = self.algo.to_string();
let secret = self.get_secret();
let period = self.step;
format!(
"otpauth://totp/{}?secret={}&issuer={}&algorithm={}&digits=6&period={}",
label, secret, issuer, algo, period
@ -991,6 +982,7 @@ mod tests {
algo: TotpAlgo::Sha256,
};
let s = totp.to_uri();
assert!(s == "otpauth://totp/blackhats%20australia:william?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30");
println!("{}", s);
assert!(s == "otpauth://totp/blackhats%20australia:william%3A%253A?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30");
}
}

View file

@ -57,6 +57,7 @@ tracing = { version = "^0.1.35", features = ["attributes"] }
tracing-serde = "^0.1.3"
tracing-subscriber = { version = "^0.3.14", features = ["env-filter"] }
url = { version = "^2.2.2", features = ["serde"] }
urlencoding = "2.1.0"
uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
validator = { version = "^0.15.0", features = ["phone"] }
webauthn-rs = "^0.3.2"

View file

@ -165,14 +165,8 @@ impl Totp {
pub fn to_proto(&self, accountname: &str, issuer: &str) -> ProtoTotp {
ProtoTotp {
accountname: accountname
.replace(":", "")
.replace("%3A", "")
.replace(" ", "%20"),
issuer: issuer
.replace(":", "")
.replace("%3A", "")
.replace(" ", "%20"),
accountname: accountname.to_string(),
issuer: issuer.to_string(),
secret: self.secret.clone(),
step: self.step,
algo: match self.algo {

View file

@ -68,7 +68,7 @@ enum Direction {
/// Display modes from the Web app manifest definition
///
/// Ref: https://developer.mozilla.org/en-US/docs/Web/Manifest/display
/// Ref: <https://developer.mozilla.org/en-US/docs/Web/Manifest/display>
#[derive(Debug, Clone, Serialize, Deserialize)]
enum DisplayMode {
/// All of the available display area is used and no user agent chrome is
@ -126,11 +126,15 @@ pub async fn manifest(req: tide::Request<AppState>) -> tide::Result {
},
];
let start_url = match req.host() {
Some(value) => format!("https://{}/", value).clone(),
None => String::from("/"),
};
let manifest_struct = Manifest {
short_name: domain_display_name.as_str(),
short_name: "Kanidm",
name: domain_display_name.as_str(),
start_url: "/", // TODO: this needs to be the frontend URL, can't get this yet
start_url: start_url.as_str(),
display_mode: DisplayMode::MinimalUi,
description: None,
orientation: None,

View file

@ -1,16 +1,159 @@
///! Custom tide middleware for Kanidm
use crate::https::JavaScriptFile;
use regex::Regex;
/// This is for the tide_compression middleware so that we only compress certain content types.
///
/// ```
/// use score::https::middleware::compression_content_type_checker;
/// let these_should_match = vec![
/// "application/wasm",
/// "text/json",
/// "text/javascript"
/// ];
/// for test_value in these_should_match {
/// eprintln!("checking {:?}", test_value);
/// assert!(compression_content_type_checker().is_match(test_value));
/// }
/// assert!(compression_content_type_checker().is_match("application/wasm"));
/// let these_should_fail = vec![
/// "image/jpeg",
/// "image/wasm",
/// "text/html",
/// ];
/// for test_value in these_should_fail {
/// eprintln!("checking {:?}", test_value);
/// assert!(!compression_content_type_checker().is_match(test_value));
/// }
/// ```
pub fn compression_content_type_checker() -> Regex {
Regex::new(r"^(?:(image/svg\+xml)|(?:application|text)/(?:css|javascript|json|text|xml|wasm))$")
.expect("regex matcher for tide_compress content-type check failed to compile")
}
#[derive(Default)]
pub struct CacheableMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for CacheableMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "max-age=60,must-revalidate,private");
Ok(response)
}
}
#[derive(Default)]
pub struct NoCacheMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for NoCacheMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "no-store, max-age=0");
response.insert_header("Pragma", "no-cache");
Ok(response)
}
}
#[derive(Default)]
/// Sets Cache-Control headers on static content endpoints
pub struct StaticContentMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StaticContentMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "max-age=3600,private");
Ok(response)
}
}
#[derive(Default)]
/// Adds the folloing headers to responses
/// - x-frame-options
/// - x-content-type-options
/// - cross-origin-resource-policy
/// - cross-origin-embedder-policy
/// - cross-origin-opener-policy
pub struct StrictResponseMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictResponseMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("cross-origin-embedder-policy", "require-corp");
response.insert_header("cross-origin-opener-policy", "same-origin");
response.insert_header("cross-origin-resource-policy", "same-origin");
response.insert_header("x-content-type-options", "nosniff");
response.insert_header("x-frame-options", "deny");
Ok(response)
}
}
struct StrictRequestMiddleware;
impl Default for StrictRequestMiddleware {
fn default() -> Self {
StrictRequestMiddleware {}
}
}
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictRequestMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let proceed = request
.header("sec-fetch-site")
.map(|hv| {
matches!(hv.as_str(), "same-origin" | "same-site" | "none")
|| (request.header("sec-fetch-mode").map(|v| v.as_str()) == Some("navigate")
&& request.method() == tide::http::Method::Get
&& request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("object")
&& request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("embed"))
})
.unwrap_or(true);
if proceed {
Ok(next.run(request).await)
} else {
Err(tide::Error::from_str(
tide::StatusCode::MethodNotAllowed,
"StrictRequestViolation",
))
}
}
}
#[derive(Default)]
/// This tide MiddleWare adds headers like Content-Security-Policy
/// and similar families. If it keeps adding more things then
/// probably rename the middleware :)
pub struct UIContentSecurityPolicyResponseMiddleware {
// The sha384 hash of /pkg/wasmloader.js
pub integrity_wasmloader: String,
pub hashes: Vec<JavaScriptFile>,
}
impl UIContentSecurityPolicyResponseMiddleware {
pub fn new(integrity_wasmloader: String) -> Self {
return Self {
integrity_wasmloader,
};
pub fn new(hashes: Vec<JavaScriptFile>) -> Self {
return Self { hashes };
}
}
@ -26,10 +169,13 @@ impl<State: Clone + Send + Sync + 'static> tide::Middleware<State>
) -> tide::Result {
let mut response = next.run(request).await;
// grab the body we're intending to return at this point
let body_str = response.take_body().into_string().await?;
// update it with the hash
response.set_body(body_str.replace("==WASMHASH==", self.integrity_wasmloader.as_str()));
// a list of hashes of js files that we're sending to the user
let hashes: Vec<String> = self
.hashes
.iter()
.map(|j| format!("'{}'", j.hash))
.collect();
response.insert_header(
/* content-security-policy headers tell the browser what to trust
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
@ -39,16 +185,12 @@ impl<State: Clone + Send + Sync + 'static> tide::Middleware<State>
we should be loading, and should be really secure about that!
*/
// TODO: consider scraping the other js files that wasm-pack builds and including them too
"content-security-policy",
vec![
"default-src 'self'",
// TODO: #912 have a dev/test mode where we can rebuild the hashes on page load, so when doing constant JS changes/rebuilds we don't have to restart the server every time. It'd be *terrible* to run in prod because of the constant disk thrashing, but nicer for devs.
// we need unsafe-eval because of WASM things
format!(
"script-src 'self' 'sha384-{}' 'unsafe-eval'",
self.integrity_wasmloader.as_str()
)
.as_str(),
format!("script-src 'self' {} 'unsafe-eval'", hashes.join(" ")).as_str(),
"form-action https: 'self'", // to allow for OAuth posts
// we are not currently using workers so it can be blocked
"worker-src 'none'",

View file

@ -1,5 +1,5 @@
mod manifest;
mod middleware;
pub mod middleware;
mod oauth2;
mod v1;
@ -15,7 +15,6 @@ use kanidm::config::{ServerRole, TlsConfiguration};
use kanidm::prelude::*;
use kanidm::status::StatusActor;
use kanidm::tracing_tree::TreeMiddleware;
use regex::Regex;
use serde::Serialize;
use std::fs::canonicalize;
use std::path::PathBuf;
@ -25,6 +24,35 @@ use tide_openssl::TlsListener;
use tracing::{error, info};
use uuid::Uuid;
#[derive(Clone)]
pub struct JavaScriptFile {
// Relative to the pkg/ dir
filepath: &'static str,
// SHA384 hash of the file
hash: String,
// if it's a module add the "type"
filetype: Option<String>,
}
impl JavaScriptFile {
/// return the hash for use in CSP headers
pub fn as_csp_hash(self) -> String {
self.hash
}
/// returns a <script> HTML tag
fn as_tag(self) -> String {
let typeattr = match self.filetype {
Some(val) => format!("type=\"{}\" ", val),
_ => String::from(""),
};
format!(
r#"<script src="/pkg/{}" integrity="{}" {}></script>"#,
self.filepath, self.hash, typeattr,
)
}
}
#[derive(Clone)]
pub struct AppState {
pub status_ref: &'static StatusActor,
@ -33,36 +61,10 @@ pub struct AppState {
// Store the token management parts.
pub jws_signer: std::sync::Arc<JwsSigner>,
pub jws_validator: std::sync::Arc<JwsValidator>,
/// The SHA384 hashes of javascript files we're going to serve to users
pub js_files: Vec<JavaScriptFile>,
}
/// This is for the tide_compression middleware so that we only compress certain content types.
///
/// ```
/// use score::https::compression_content_type_checker;
/// let these_should_match = vec![
/// "application/wasm",
/// "text/json",
/// "text/javascript"
/// ];
/// for test_value in these_should_match {
/// eprintln!("checking {:?}", test_value);
/// assert!(compression_content_type_checker().is_match(test_value));
/// }
/// assert!(compression_content_type_checker().is_match("application/wasm"));
/// let these_should_fail = vec![
/// "image/jpeg",
/// "image/wasm",
/// "text/html",
/// ];
/// for test_value in these_should_fail {
/// eprintln!("checking {:?}", test_value);
/// assert!(!compression_content_type_checker().is_match(test_value));
/// }
/// ```
pub fn compression_content_type_checker() -> Regex {
Regex::new(r"^(?:(image/svg\+xml)|(?:application|text)/(?:css|javascript|json|text|xml|wasm))$")
.expect("regex matcher for tide_compress content-type check failed to compile")
}
pub trait RequestExtensions {
fn get_current_uat(&self) -> Option<String>;
@ -182,7 +184,7 @@ pub fn to_tide_response<T: Serialize>(
})
}
/// Returns a generic robots.txt
/// Returns a generic robots.txt blocking all bots
async fn robots_txt(_req: tide::Request<AppState>) -> tide::Result {
let mut res = tide::Response::new(200);
@ -205,6 +207,15 @@ async fn index_view(req: tide::Request<AppState>) -> tide::Result {
res.insert_header("X-KANIDM-OPID", hvalue);
res.set_content_type("text/html;charset=utf-8");
// this feels icky but I felt that adding a trait on Vec<JavaScriptFile> which generated the string was going a bit far
let jsfiles: Vec<String> = req
.state()
.to_owned()
.js_files
.into_iter()
.map(|j| j.as_tag())
.collect();
let jstags = jsfiles.join(" ");
res.set_body(format!(r#"
<!DOCTYPE html>
<html lang="en">
@ -223,8 +234,8 @@ async fn index_view(req: tide::Request<AppState>) -> tide::Result {
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
<link rel="stylesheet" href="/pkg/style.css"/>
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"></script>
<script type="module" type="text/javascript" src="/pkg/wasmloader.js" integrity="sha384-==WASMHASH=="></script>
{}
</head>
<body class="flex-column d-flex h-100">
<main class="flex-shrink-0 form-signin">
@ -239,125 +250,16 @@ async fn index_view(req: tide::Request<AppState>) -> tide::Result {
</div>
</footer>
</body>
</html>"#, domain_display_name.as_str()
</html>"#,
domain_display_name.as_str(),
jstags,
)
);
Ok(res)
}
#[derive(Default)]
struct NoCacheMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for NoCacheMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "no-store, max-age=0");
response.insert_header("Pragma", "no-cache");
Ok(response)
}
}
#[derive(Default)]
struct CacheableMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for CacheableMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "max-age=60,must-revalidate,private");
Ok(response)
}
}
#[derive(Default)]
/// Sets Cache-Control headers on static content endpoints
struct StaticContentMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StaticContentMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "max-age=3600,private");
Ok(response)
}
}
#[derive(Default)]
/// Adds the folloing headers to responses
/// - x-frame-options
/// - x-content-type-options
/// - cross-origin-resource-policy
/// - cross-origin-embedder-policy
/// - cross-origin-opener-policy
struct StrictResponseMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictResponseMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("cross-origin-embedder-policy", "require-corp");
response.insert_header("cross-origin-opener-policy", "same-origin");
response.insert_header("cross-origin-resource-policy", "same-origin");
response.insert_header("x-content-type-options", "nosniff");
response.insert_header("x-frame-options", "deny");
Ok(response)
}
}
struct StrictRequestMiddleware;
impl Default for StrictRequestMiddleware {
fn default() -> Self {
StrictRequestMiddleware {}
}
}
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictRequestMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let proceed = request
.header("sec-fetch-site")
.map(|hv| {
matches!(hv.as_str(), "same-origin" | "same-site" | "none")
|| (request.header("sec-fetch-mode").map(|v| v.as_str()) == Some("navigate")
&& request.method() == tide::http::Method::Get
&& request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("object")
&& request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("embed"))
})
.unwrap_or(true);
if proceed {
Ok(next.run(request).await)
} else {
Err(tide::Error::from_str(
tide::StatusCode::MethodNotAllowed,
"StrictRequestViolation",
))
}
}
}
/// Generates the integrity hash for a file based on a filename
pub fn generate_integrity_hash(filename: String) -> Result<String, String> {
let wasm_filepath = PathBuf::from(filename);
match wasm_filepath.exists() {
@ -379,7 +281,7 @@ pub fn generate_integrity_hash(filename: String) -> Result<String, String> {
};
let shasum =
openssl::hash::hash(openssl::hash::MessageDigest::sha384(), &filecontents).unwrap();
Ok(format!("{}", openssl::base64::encode_block(&shasum)))
Ok(format!("sha384-{}", openssl::base64::encode_block(&shasum)))
}
}
}
@ -403,12 +305,44 @@ pub fn create_https_server(
let jws_validator = std::sync::Arc::new(jws_validator);
let jws_signer = std::sync::Arc::new(jws_signer);
let mut js_files: Vec<JavaScriptFile> = Vec::new();
if !matches!(role, ServerRole::WriteReplicaNoUI) {
// let's set up the list of js module hashes
for filepath in ["wasmloader.js"] {
js_files.push(JavaScriptFile {
filepath,
hash: generate_integrity_hash(format!(
"{}/{}",
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
filepath,
))
.unwrap(),
filetype: Some("module".to_string()),
});
}
// let's set up the list of non-module hashes
for filepath in ["external/bootstrap.bundle.min.js"] {
js_files.push(JavaScriptFile {
filepath,
hash: generate_integrity_hash(format!(
"{}/{}",
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
filepath,
))
.unwrap(),
filetype: None,
});
}
};
let mut tserver = tide::Server::with_state(AppState {
status_ref,
qe_w_ref,
qe_r_ref,
jws_signer,
jws_validator,
js_files: js_files.to_owned(),
});
// tide::log::with_level(tide::log::LevelFilter::Debug);
@ -467,10 +401,7 @@ pub fn create_https_server(
let mut static_tserver = tserver.at("");
static_tserver.with(StaticContentMiddleware::default());
static_tserver.with(UIContentSecurityPolicyResponseMiddleware::new(
generate_integrity_hash(env!("KANIDM_WEB_UI_PKG_PATH").to_owned() + "/wasmloader.js")
.unwrap(),
));
static_tserver.with(UIContentSecurityPolicyResponseMiddleware::new(js_files));
// The compression middleware needs to be the last one added before routes
static_tserver.with(compress_middleware.clone());

View file

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 135.46667 135.46666"
version="1.1"
id="svg1084"
sodipodi:docname="kani-waving.svg"
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
inkscape:export-filename="logo-180.png"
inkscape:export-xdpi="33.75"
inkscape:export-ydpi="33.75"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title332">Kanidm Square Logo</title>
<sodipodi:namedview
id="namedview39"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="true"
inkscape:zoom="1"
inkscape:cx="178"
inkscape:cy="304.5"
inkscape:window-width="1389"
inkscape:window-height="847"
inkscape:window-x="51"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg1084">
<sodipodi:guide
position="-52.916665,67.733332"
orientation="0,1"
id="guide145"
inkscape:locked="false"
inkscape:label="vertical_middle"
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="121.97292,175.15416"
orientation="-1,0"
id="guide777"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="13.49375,173.03749"
orientation="-1,0"
id="guide779"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="22.754166,121.97292"
orientation="0,1"
id="guide781"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="4.4979168,13.758333"
orientation="0,1"
id="guide783"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1081" />
<metadata
id="metadata330">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Kanidm Square Logo</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="matrix(1.0952772,0,0,1.0952772,-42.847275,-49.68044)"
inkscape:label="kani"
style="display:inline">
<path
id="path5404"
d="m 62.537712,110.23377 c -3.36297,8.12708 5.78522,15.61876 5.82446,15.73981 -1.67096,-4.90156 -3.43697,-8.85836 -0.189,-12.28424"
style="display:inline;fill:#803300;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="display:inline;fill:#803300;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 138.4423,109.69923 c 3.36297,8.12708 -5.78522,15.61876 -5.82446,15.7398 1.67096,-4.90155 3.43697,-8.85835 0.189,-12.28423"
id="path5408" />
<path
id="path4529"
d="m 100.24698,67.21217 c -1.308628,-0.01556 -2.560878,2.352773 -4.890668,6.986654 -7.05187,-7.755768 -7.41258,-7.849666 -9.84229,2.649966 -0.11364,0.04758 -0.22709,0.0959 -0.34055,0.144695 -8.88231,-5.324406 -9.34124,-5.366051 -8.66717,5.041551 -0.21569,0.159933 -0.43121,0.322088 -0.64596,0.486794 -9.102,-3.605578 -9.49555,-3.378386 -7.19232,6.611477 -0.10784,0.11671 -0.21599,0.23284 -0.3235,0.35089 -9.27685,-2.506375 -9.53641,-2.129764 -6.07663,7.65845 -0.14847,0.21295 -0.29578,0.42957 -0.44338,0.64544 -10.45618,-1.11775 -10.67098,-0.91353 -5.35523,8.828413 -0.026,0.0482 -0.0526,0.0943 -0.0786,0.14263 -4.28381,2.04782 -5.70197,4.54381 -7.73183,7.89409 -0.15756,6.64457 6.9544,12.37105 17.18035,18.87895 -3.51325,-6.5113 -9.00735,-9.81207 -10.0614,-17.88832 1.54367,-2.90538 4.16877,-3.90901 6.42286,-5.21622 24.61995,14.36815 50.615808,14.53471 76.550928,-0.55035 2.26015,1.31503 4.90084,2.31509 6.45076,5.23224 -1.05405,8.07624 -6.54866,11.3765 -10.06191,17.8878 10.22594,-6.5079 17.33842,-12.23387 17.18086,-18.87844 -1.97354,-3.25733 -3.37677,-5.706 -7.38973,-7.72149 5.01291,-9.218263 4.53571,-9.305023 -5.98567,-8.177293 -0.0891,-0.1152 -0.17795,-0.23186 -0.26717,-0.34623 3.63467,-10.261532 3.40665,-10.529692 -6.39186,-7.87136 2.47993,-10.705895 2.14854,-10.749554 -7.77938,-6.803718 0.74781,-11.285018 0.47993,-11.261581 -8.88835,-5.642548 -2.49815,-10.818768 -2.79293,-10.786576 -9.94048,-2.924887 -2.68846,-4.933638 -4.10296,-7.403383 -5.43173,-7.419184 z"
style="display:inline;fill:#ff6600;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path5389"
d="m 75.334492,106.8255 c -0.51041,0.004 -0.85068,0.0544 -0.79478,0.0889 0,0 -9.13406,11.93611 0.31006,18.65881 a 9.7471106,8.2770874 37.793943 0 0 1.43712,9.42372 9.7471106,8.2770874 37.793943 0 0 10.56473,3.9672 l -3.56412,-8.60569 9.19685,0.73536 a 9.7471106,8.2770874 37.793943 0 0 -7.02645,-8.91057 9.7471106,8.2770874 37.793943 0 0 -7.00784,0.009 c -1.86231,-1.36401 -4.6287,-4.92596 -0.0408,-12.47521 1.51807,-2.49795 -1.54349,-2.90314 -3.07475,-2.89129 z"
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="path5389" />
<g
id="g5758"
transform="translate(-2.3193819,-7.5517449)"
style="display:inline"
inkscape:label="kani">
<path
style="display:inline;fill:#d45500;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 98.808992,121.55238 c -0.682767,-0.0559 4.468828,10.17565 8.051908,0.17277 0.24133,-0.67372 -3.69223,2.90976 -8.051908,-0.17277 z"
id="path5399" />
<g
id="g351">
<path
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 93.212672,111.5632 c -5.131386,10.99206 5.618332,14.73472 4.412511,4.11951 -0.319289,-3.2881 -2.328089,-7.17448 -4.412511,-4.11951 z"
id="path5415" />
<path
style="fill:#ffffff;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 95.394393,112.41429 c -1.183441,1.51896 -0.08172,3.83605 -0.09872,3.98526 1.674403,1.64132 1.61267,-4.77034 0.09872,-3.98526 z"
id="path5417" />
</g>
<g
id="g355">
<path
id="path5433"
d="m 109.6169,111.5632 c -5.13141,10.99206 5.61836,14.73472 4.41254,4.11951 -0.31929,-3.2881 -2.3281,-7.17448 -4.41254,-4.11951 z"
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="path5435"
d="m 111.79864,112.41429 c -1.18345,1.51896 -0.0817,3.83605 -0.0987,3.98526 1.67441,1.64132 1.61267,-4.77034 0.0987,-3.98526 z"
style="display:inline;fill:#ffffff;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</g>
<path
id="path5732"
d="m 111.51646,83.878113 c -0.0644,2.604234 0.83829,5.245703 2.49515,7.301842 2.19739,2.716867 5.40747,4.034785 8.37885,3.439994 6.01625,8.488971 -0.30315,11.998681 -0.4866,12.038481 -1.32922,0.42046 0.0534,6.32365 4.60901,2.5271 7.11608,-5.93031 2.73673,-13.62732 0.0641,-17.143859 1.54476,-1.912559 2.20351,-4.57778 1.80861,-7.317326 -0.6031,-4.141273 -3.48425,-7.674191 -7.13223,-8.745669 l -1.25194,9.229861 -1.06142,-0.736985 z"
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="arm-up"
transform="matrix(1.0952772,0,0,1.0952772,-42.847275,-49.680439)">
<set id="show_frame0" attributeName="visibility" attributeType="CSS" to="visible"
begin="0ms; hide_frame1.end" dur="500ms" fill="freeze"/>
<set id="hide_frame0" attributeName="visibility" attributeType="CSS" to="hidden"
begin="show_frame0.end" dur="1ms" fill="freeze"/>
</path>
<path
id="path294"
d="m 136.84733,83.996879 c -2.18137,1.42401 -3.84498,3.66555 -4.59874,6.19631 -0.99138,3.35068 -0.25622,6.742 1.91889,8.85192 -3.57942,9.769591 -10.05414,6.555411 -10.19096,6.426891 -1.10017,-0.85629 -5.17794,3.63047 0.53268,5.22929 8.92019,2.49747 12.77572,-5.47478 14.1562,-9.67038 2.45132,0.18756 5.02004,-0.78148 7.05239,-2.660471 3.06875,-2.84546 4.34444,-7.22212 3.15795,-10.83433 l -8.31186,4.20365 0.005,-1.29218 z"
style="display:inline;fill:#ff6600;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="arm-down"
transform="matrix(1.0952772,0,0,1.0952772,-42.847275,-49.680439)" visibility="hidden">
<set id="show_frame1" attributeName="visibility" attributeType="CSS" to="visible"
begin="hide_frame0.end" dur="500ms" fill="freeze"/>
<set id="hide_frame1" attributeName="visibility" attributeType="CSS" to="hidden"
begin="show_frame1.end" dur="1ms" fill="freeze"/>
</path>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -1,14 +1,10 @@
import { modal_hide } from './snippets/kanidmd_web_ui-273c66c330cf4c44/inline0.js';
import { modal_hide_by_id } from '/pkg/wasmloader.js';
let wasm;
const heap = new Array(32).fill(undefined);
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let WASM_VECTOR_LEN = 0;
cachedTextDecoder.decode();
let cachedUint8Memory0;
function getUint8Memory0() {
@ -18,6 +14,31 @@ function getUint8Memory0() {
return cachedUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
const heap = new Array(32).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
if (typeof(heap_next) !== 'number') throw new Error('corrupt heap');
heap[idx] = obj;
return idx;
}
function getObject(idx) { return heap[idx]; }
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = new TextEncoder('utf-8');
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
@ -73,6 +94,10 @@ function passStringToWasm0(arg, malloc, realloc) {
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedInt32Memory0;
function getInt32Memory0() {
if (cachedInt32Memory0.byteLength === 0) {
@ -81,31 +106,6 @@ function getInt32Memory0() {
return cachedInt32Memory0;
}
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
if (typeof(heap_next) !== 'number') throw new Error('corrupt heap');
heap[idx] = obj;
return idx;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
function _assertBoolean(n) {
if (typeof(n) !== 'boolean') {
throw new Error('expected a boolean argument');
@ -367,17 +367,6 @@ async function load(module, imports) {
function getImports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_modalhide_673016763df325bd = function() { return logError(function (arg0, arg1) {
modal_hide(getStringFromWasm0(arg0, arg1));
}, arguments) };
imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = JSON.stringify(obj === undefined ? null : obj);
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
@ -394,6 +383,17 @@ function getImports() {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbg_modalhidebyid_b9efcd5f48cb1c79 = function() { return logError(function (arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
}, arguments) };
imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = JSON.stringify(obj === undefined ? null : obj);
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbindgen_json_parse = function(arg0, arg1) {
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
@ -453,6 +453,11 @@ function getImports() {
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbg_debug_5a27eb2cb0d074ba = function() { return logError(function (arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.debug(...v0);
}, arguments) };
imports.wbg.__wbg_log_06b7ffc63a0f8bee = function() { return logError(function (arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
@ -866,16 +871,16 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper19331 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1306, __wbg_adapter_30);
imports.wbg.__wbindgen_closure_wrapper19372 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1308, __wbg_adapter_30);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_closure_wrapper23261 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1331, __wbg_adapter_33);
imports.wbg.__wbindgen_closure_wrapper23302 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1333, __wbg_adapter_33);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_closure_wrapper23862 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1357, __wbg_adapter_36);
imports.wbg.__wbindgen_closure_wrapper23903 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1359, __wbg_adapter_36);
return addHeapObject(ret);
}, arguments) };

View file

@ -1,5 +0,0 @@
export function modal_hide(m) {
var elem = document.getElementById(m);
var modal = bootstrap.Modal.getInstance(elem);
modal.hide();
}

View file

@ -19,7 +19,6 @@ body {
}
.form-signin {
/* width: 100%; */
max-width: 680px;
margin: auto;
}

View file

@ -5,3 +5,11 @@ async function main() {
run_app();
}
main()
// this is used in
export function modal_hide_by_id(m) {
var elem = document.getElementById(m);
var modal = bootstrap.Modal.getInstance(elem);
modal.hide();
};

View file

@ -0,0 +1,7 @@
///! Constants
// CSS classes that get applied to full-page forms
pub const CSS_CLASSES_BODY_FORM: &[&str] = &["flex-column", "d-flex", "h-100"];
// the HTML element ID that the signout modal dialogue box has
pub const ID_SIGNOUTMODAL: &str = "signoutModal";

View file

@ -74,9 +74,7 @@ impl DeleteApp {
let jsval = JsFuture::from(resp.json()?).await?;
let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type");
EventBus::dispatcher().send(EventBusMsg::UpdateStatus {
status: status.clone(),
});
EventBus::dispatcher().send(EventBusMsg::UpdateStatus { status });
Ok(Msg::Success)
} else {
@ -142,10 +140,7 @@ impl Component for DeleteApp {
fn view(&self, ctx: &Context<Self>) -> Html {
console::log!("delete modal::view");
let submit_enabled = match &self.state {
State::Init => true,
_ => false,
};
let submit_enabled = matches!(&self.state, State::Init);
html! {
<div class="modal fade" id="staticDeletePrimaryCred" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticDeletePrimaryCred" aria-hidden="true">

View file

@ -224,10 +224,10 @@ impl Component for PwModalApp {
PwCheck::Invalid => classes!("form-control", "is-invalid"),
};
let submit_enabled = match (&self.state, &self.pw_check) {
(PwState::Feedback(_), PwCheck::Valid) | (PwState::Init, PwCheck::Valid) => true,
_ => false,
};
let submit_enabled = matches!(
(&self.state, &self.pw_check),
(PwState::Feedback(_), PwCheck::Valid) | (PwState::Init, PwCheck::Valid),
);
let pw_val = self.pw_val.clone();
let pw_check_val = self.pw_check_val.clone();

View file

@ -54,6 +54,8 @@ impl From<FetchError> for Msg {
}
}
#[allow(clippy::large_enum_variant)]
//Page state
enum State {
TokenInput,
WaitingForStatus,
@ -62,6 +64,7 @@ enum State {
status: CUStatus,
},
WaitingForCommit,
#[allow(clippy::large_enum_variant)]
Error {
emsg: String,
kopid: Option<String>,
@ -87,10 +90,7 @@ impl Component for CredentialResetApp {
// Where did we come from?
// Inject our class to centre everything.
if let Err(e) = crate::utils::body().class_list().add_1("form-signin-body") {
console::log!(format!("class_list add error -> {:?}", e));
};
add_body_form_classes!();
// Can we pre-load in a session token? This occures when we are sent a
// credential reset from the views UI.
@ -195,7 +195,10 @@ impl Component for CredentialResetApp {
None
}
(Msg::Error { emsg, kopid }, _) => Some(State::Error { emsg, kopid }),
(_, _) => unreachable!(),
(_, _) => {
console::debug!("CredentialResetApp state match fail on update.");
None
}
};
if let Some(mut next_state) = next_state {
@ -216,50 +219,49 @@ impl Component for CredentialResetApp {
match &self.state {
State::TokenInput => self.view_token_input(ctx),
State::WaitingForStatus | State::WaitingForCommit => self.view_waiting(ctx),
State::Main { token, status } => self.view_main(ctx, &token, &status),
State::Error { emsg, kopid } => self.view_error(ctx, &emsg, kopid.as_deref()),
State::Main { token, status } => self.view_main(ctx, token, status),
State::Error { emsg, kopid } => self.view_error(ctx, emsg, kopid.as_deref()),
}
}
fn destroy(&mut self, _ctx: &Context<Self>) {
console::log!("credential::reset::destroy");
if let Err(e) = crate::utils::body()
.class_list()
.remove_1("form-signin-body")
{
console::log!(format!("class_list remove error -> {:?}", e));
}
remove_body_form_classes!();
}
}
impl CredentialResetApp {
fn view_token_input(&self, ctx: &Context<Self>) -> Html {
html! {
<main class="form-signin">
<div class="container">
<main class="flex-shrink-0 form-signin">
<center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
// TODO: replace this with a call to domain info
<h3>{ "Kanidm idm.example.com" } </h3>
</center>
<p>
{"Enter your credential reset token"}
</p>
</div>
<div class="container">
<form
<form
onsubmit={ ctx.link().callback(|e: FocusEvent| {
console::log!("credential::reset::view_token_input -> TokenInput - prevent_default()");
e.prevent_default();
Msg::TokenSubmit
} ) }
action="javascript:void(0);"
>
action="javascript:void(0);">
<input
id="autofocus"
type="text"
class="form-control"
value=""
/>
<button type="submit" class="btn btn-dark">{" Submit "}</button>
<button type="submit" class="btn btn-dark">{" Submit "}</button><br />
</form>
</div>
<p>
<a href="/"><button href="/" class="btn btn-dark" aria-label="Return home">{"Return to the home page"}</button></a>
</p>
</main>
}
}
@ -277,12 +279,7 @@ impl CredentialResetApp {
}
fn view_main(&self, ctx: &Context<Self>, token: &CUSessionToken, status: &CUStatus) -> Html {
if let Err(e) = crate::utils::body()
.class_list()
.remove_1("form-signin-body")
{
console::log!(format!("class_list remove error -> {:?}", e));
}
remove_body_form_classes!();
let displayname = status.displayname.clone();
let spn = status.spn.clone();

View file

@ -251,10 +251,7 @@ impl Component for TotpModalApp {
};
let submit_enabled = match &self.state {
TotpState::Init => true,
_ => false,
};
let submit_enabled = matches!(&self.state, TotpState::Init);
let submit_button = match &self.check {
TotpCheck::Sha1Accept => html! {
@ -294,6 +291,7 @@ impl Component for TotpModalApp {
let svg = qr.render::<svg::Color>().build();
#[allow(clippy::unwrap_used)]
let div = utils::document().create_element("div").unwrap();
div.set_inner_html(svg.as_str());

View file

@ -0,0 +1 @@
../../../artwork/kani-waving.svg

View file

@ -13,6 +13,10 @@
use wasm_bindgen::prelude::*;
#[macro_use]
mod macros;
mod constants;
mod credential;
mod error;
mod login;

View file

@ -43,7 +43,6 @@ enum LoginState {
Authenticated,
}
const CLASSES_TO_ADD: &[&str] = &["flex-column", "d-flex", "h-100"];
pub enum LoginAppMsg {
Input(String),
Restart,
@ -468,13 +467,8 @@ impl Component for LoginApp {
console::log!(cookie);
let state = LoginState::Init(true);
// startConfetti();
for x in CLASSES_TO_ADD {
if let Err(e) = crate::utils::body().class_list().add_1(x) {
console::log!(format!("class_list add error -> {:?}", e));
};
}
add_body_form_classes!();
LoginApp {
inputvalue,
@ -780,6 +774,7 @@ impl Component for LoginApp {
<main class="flex-shrink-0 form-signin">
<center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
// TODO: replace this with a call to domain info
<h3>{ "Kanidm idm.example.com" } </h3>
</center>
{ self.view_state(ctx) }
@ -795,19 +790,7 @@ impl Component for LoginApp {
fn destroy(&mut self, _ctx: &Context<Self>) {
console::log!("login::destroy".to_string());
for x in CLASSES_TO_ADD {
if let Err(e) = crate::utils::body().class_list().remove_1(x) {
console::log!(format!("class_list remove error -> {:?}", e));
};
}
// if let Err(e) = crate::utils::body()
// .class_list()
// .remove_1("form-signin-body")
// {
// console::log!(format!("class_list remove error -> {:?}", e));
// }
remove_body_form_classes!();
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {

View file

@ -0,0 +1,25 @@
///! Macros for the web UI
/// Adds a set of CSS classes to the body element when we're presenting a full-page form
#[macro_export]
macro_rules! add_body_form_classes {
() => {
for x in $crate::constants::CSS_CLASSES_BODY_FORM {
if let Err(e) = $crate::utils::body().class_list().add_1(x) {
console::log!(format!("class_list add error -> {:?}", e));
};
}
};
}
/// Removes the set of CSS classes from the body element after we're presenting a full-page form
#[macro_export]
macro_rules! remove_body_form_classes {
() => {
for x in $crate::constants::CSS_CLASSES_BODY_FORM {
if let Err(e) = $crate::utils::body().class_list().remove_1(x) {
console::log!(format!("class_list removal error -> {:?}", e));
};
}
};
}

View file

@ -247,9 +247,7 @@ impl Component for Oauth2App {
models::pop_oauth2_authorisation_request()
});
if let Err(e) = crate::utils::body().class_list().add_1("form-signin-body") {
console::log!(format!("class_list add error -> {:?}", e).as_str());
};
add_body_form_classes!();
// If we have neither we need to say that we can not proceed at all.
let query = match query {
@ -498,11 +496,6 @@ impl Component for Oauth2App {
fn destroy(&mut self, _ctx: &Context<Self>) {
console::log!("oauth2::destroy");
if let Err(e) = crate::utils::body()
.class_list()
.remove_1("form-signin-body")
{
console::log!(format!("class_list remove error -> {:?}", e).as_str());
}
remove_body_form_classes!();
}
}

View file

@ -19,7 +19,6 @@ body {
}
.form-signin {
/* width: 100%; */
max-width: 680px;
margin: auto;
}

View file

@ -62,15 +62,7 @@ pub fn get_value_from_element_id(id: &str) -> Option<String> {
.map(|element| element.value())
}
#[wasm_bindgen(inline_js = "export function modal_hide(m) {
var elem = document.getElementById(m);
var modal = bootstrap.Modal.getInstance(elem);
modal.hide();
}")]
#[wasm_bindgen(raw_module = "/pkg/wasmloader.js")]
extern "C" {
fn modal_hide(m: &str);
}
pub fn modal_hide_by_id(id: &str) {
modal_hide(id);
pub fn modal_hide_by_id(m: &str);
}

View file

@ -198,59 +198,82 @@ impl Component for ViewsApp {
}
impl ViewsApp {
/// The base page for the user dashboard
fn view_authenticated(&self, ctx: &Context<Self>) -> Html {
// WARN set dash-body against body here?
html! {
<div class="dash-body">
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">{ "Kanidm" }</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-nav">
<div class="nav-item text-nowrap">
<a class="nav-link px-3" href="#" onclick={ ctx.link().callback(|_| ViewsMsg::Logout) } >{ "Sign out" }</a>
</div>
</div>
</header>
<>
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{ViewRoute::Profile}">{"Kanidm"}</a>
<button class="navbar-toggler bg-light" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<img src="/pkg/img/favicon.png" />
</button>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Apps}>
<span data-feather="file"></span>
{ "Apps" }
</Link<ViewRoute>>
</li>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="mb-1">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Apps}>
<span data-feather="file"></span>
{ "Apps" }
</Link<ViewRoute>>
</li>
<li class="nav-item">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Profile}>
<span data-feather="file"></span>
{ "Profile" }
</Link<ViewRoute>>
</li>
<li class="nav-item">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Security}>
<span data-feather="file"></span>
{ "Security" }
</Link<ViewRoute>>
</li>
<li class="mb-1">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Profile}>
<span data-feather="file"></span>
{ "Profile" }
</Link<ViewRoute>>
</li>
<li class="mb-1">
<Link<ViewRoute> classes="nav-link" to={ViewRoute::Security}>
<span data-feather="file"></span>
{ "Security" }
</Link<ViewRoute>>
</li>
<li class="mb-1">
<a class="nav-link" href="#"
data-bs-toggle="modal"
data-bs-target={format!("#{}", crate::constants::ID_SIGNOUTMODAL)}
>{"Sign out"}</a>
</li>
</ul>
<form class="d-flex">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" />
<button class="btn btn-outline-light" type="submit">{"Search"}</button>
</form>
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<Switch<ViewRoute> render={ Switch::render(switch) } />
</main>
</div>
</nav>
// sign out modal dialogue box
<div class="modal" tabindex="-1" role="dialog" id={crate::constants::ID_SIGNOUTMODAL}>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Confirm Sign out"}</h5>
</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" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success"
data-bs-toggle="modal"
data-bs-target={format!("#{}", crate::constants::ID_SIGNOUTMODAL)}
onclick={ ctx.link().callback(|_| ViewsMsg::Logout) }>{ "Sign out" }</button>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal"
>{"Cancel"}</button>
</div>
</div>
</div>
</div>
<main class="p-3 x-auto">
<Switch<ViewRoute> render={ Switch::render(switch) } />
</main>
</>
}
}

View file

@ -17,6 +17,8 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
#[allow(clippy::large_enum_variant)]
// Page state
pub enum Msg {
// Nothing
RequestCredentialUpdate,

View file

@ -5,3 +5,11 @@ async function main() {
run_app();
}
main()
// this is used in
export function modal_hide_by_id(m) {
var elem = document.getElementById(m);
var modal = bootstrap.Modal.getInstance(elem);
modal.hide();
};