Splitting the SPAs (#2219)

* doing some work for enumerating how the accounts work together
* fixing up build scripts and removing extra things
* making JavaScript as_tag use the struct field names
* making shared.js a module, removing wasmloader.js
* don't compress compressed things
This commit is contained in:
James Hodgkinson 2023-10-27 16:03:58 +10:00 committed by GitHub
parent ad3c491d07
commit e02328ae8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 8556 additions and 1459 deletions

11
.codespell_ignore Normal file
View file

@ -0,0 +1,11 @@
alledges
crate
unexpect
Pres
pres
ACI
aci
ser
te
ue
unx

4
.gitignore vendored
View file

@ -30,3 +30,7 @@ kanidm-client-tools.tar.gz
.coverage
pykanidm/dist/
pykanidm/site/
# oauth2 integration test things
scripts/oauth_proxy/client.secret
scripts/oauth_proxy/envfile

101
Cargo.lock generated
View file

@ -2259,7 +2259,7 @@ dependencies = [
"gloo-render",
"gloo-storage",
"gloo-timers 0.2.6",
"gloo-utils",
"gloo-utils 0.1.7",
"gloo-worker",
]
@ -2269,7 +2269,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [
"gloo-utils",
"gloo-utils 0.1.7",
"js-sys",
"serde",
"wasm-bindgen",
@ -2316,7 +2316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
dependencies = [
"gloo-events",
"gloo-utils",
"gloo-utils 0.1.7",
"serde",
"serde-wasm-bindgen 0.5.0",
"serde_urlencoded",
@ -2334,7 +2334,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"gloo-utils 0.1.7",
"http",
"js-sys",
"pin-project",
@ -2362,7 +2362,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [
"gloo-utils",
"gloo-utils 0.1.7",
"js-sys",
"serde",
"serde_json",
@ -2406,6 +2406,19 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-worker"
version = "0.2.1"
@ -2415,7 +2428,7 @@ dependencies = [
"anymap2",
"bincode",
"gloo-console",
"gloo-utils",
"gloo-utils 0.1.7",
"js-sys",
"serde",
"wasm-bindgen",
@ -3059,6 +3072,7 @@ dependencies = [
"filetime",
"futures",
"futures-util",
"hashbrown 0.14.2",
"http",
"hyper",
"kanidm_build_profiles",
@ -3196,13 +3210,85 @@ dependencies = [
]
[[package]]
name = "kanidmd_web_ui"
name = "kanidmd_web_ui_admin"
version = "1.1.0-rc.14-dev"
dependencies = [
"gloo",
"gloo-utils 0.2.0",
"js-sys",
"kanidm_proto",
"kanidmd_web_ui_shared",
"lazy_static",
"serde",
"serde-wasm-bindgen 0.5.0",
"serde_json",
"time",
"url",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-sys",
"yew",
"yew-router",
]
[[package]]
name = "kanidmd_web_ui_login_flows"
version = "1.1.0-rc.14-dev"
dependencies = [
"gloo",
"gloo-utils 0.2.0",
"js-sys",
"kanidm_proto",
"kanidmd_web_ui_shared",
"lazy_static",
"serde",
"serde-wasm-bindgen 0.5.0",
"serde_json",
"time",
"url",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-sys",
"yew",
"yew-router",
]
[[package]]
name = "kanidmd_web_ui_shared"
version = "1.1.0-rc.14-dev"
dependencies = [
"gloo",
"js-sys",
"kanidm_proto",
"lazy_static",
"serde",
"serde-wasm-bindgen 0.5.0",
"serde_json",
"time",
"url",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-sys",
"yew",
"yew-router",
]
[[package]]
name = "kanidmd_web_ui_user"
version = "1.1.0-rc.14-dev"
dependencies = [
"gloo",
"gloo-timers 0.3.0",
"gloo-utils 0.2.0",
"js-sys",
"kanidm_proto",
"kanidmd_web_ui_shared",
"lazy_static",
"qrcode",
"regex",
@ -4088,6 +4174,7 @@ dependencies = [
"fixedbitset",
"indexmap 2.0.2",
"serde",
"serde_derive",
]
[[package]]

View file

@ -13,7 +13,10 @@ members = [
"unix_integration",
"unix_integration/nss_kanidm",
"unix_integration/pam_kanidm",
"server/web_ui",
"server/web_ui/admin",
"server/web_ui/login_flows",
"server/web_ui/user",
"server/web_ui/shared",
"server/daemon",
"server/lib",
"server/lib-macros",
@ -125,6 +128,7 @@ futures-concurrency = "^3.1.0"
futures-util = { version = "^0.3.21", features = ["sink"] }
gix = { version = "0.53.1", default-features = false }
gloo = "^0.8.1"
gloo-utils = "0.2.0"
hashbrown = { version = "0.14.2", features = ["serde", "inline-more", "ahash"] }
hex = "^0.4.3"
http = "0.2.9"
@ -138,6 +142,7 @@ image = { version = "0.24.7", default-features = false, features = [
] }
enum-iterator = "1.4.0"
js-sys = "^0.3.63"
kanidmd_web_ui_shared = { path = "./server/web_ui/shared" }
# REMOVE this
lazy_static = "^1.4.0"
ldap3_client = "^0.3.5"

View file

@ -133,7 +133,7 @@ install-tools:
codespell: ## spell-check things.
codespell:
codespell -c \
-L 'crate,unexpect,Pres,pres,ACI,aci,ser,te,ue,unx,aNULL' \
--ignore-words .codespell_ignore \
--skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache,./pykanidm/poetry.lock' \
--skip='./book/*.js' \
--skip='./book/book/*' \
@ -141,7 +141,10 @@ codespell:
--skip='./docs/*,./.git' \
--skip='*.svg' \
--skip='./rlm_python/mods-available/eap' \
--skip='./server/web_ui/static/external,./server/web_ui/pkg/external' \
--skip='./server/web_ui/static/external' \
--skip='./server/web_ui/pkg/external' \
--skip='./server/web_ui/shared/static/external' \
--skip='./server/web_ui/admin/static/external,./server/web_ui/admin/pkg/external' \
--skip='./server/lib/src/constants/system_config.rs,./pykanidm/site,./server/lib/src/constants/*.json'
.PHONY: test/pykanidm/pytest
@ -277,7 +280,7 @@ cert/clean:
.PHONY: webui
webui: ## Build the WASM web frontend
cd server/web_ui && ./build_wasm_release.sh
cd server/web_ui && ./build_wasm.sh
.PHONY: webui/test
webui/test: ## Run wasm-pack test

View file

@ -276,7 +276,7 @@ cd server/web_ui/
./build_wasm_dev.sh
```
To build for release, run `build_wasm_release.sh`.
To build for release, run `build_wasm.sh`, or `make webui` from the project root.
The "developer" profile for kanidmd will automatically use the pkg output in this folder.

View file

@ -24,6 +24,7 @@ use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::time::Duration;
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME, KOPID, KSESSIONID, KVERSION};
use kanidm_proto::v1::*;
use reqwest::header::CONTENT_TYPE;
@ -1442,7 +1443,7 @@ impl KanidmClient {
}
pub async fn auth_valid(&self) -> Result<(), ClientError> {
self.perform_get_request("/v1/auth/valid").await
self.perform_get_request(V1_AUTH_VALID).await
}
pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {

View file

@ -1,7 +1,7 @@
[package]
name = "kanidm_lib_crypto"
version = "0.1.0"
edition = "2021"
edition = { workspace = true }
[features]
tpm = ["dep:tss-esapi"]

View file

@ -17,7 +17,7 @@ doctest = false
[dependencies]
[target.'cfg(target_family = "windows")'.dependencies]
whoami = {workspace = true}
whoami = { workspace = true }
[target.'cfg(not(target_family = "windows"))'.dependencies]
kanidm_utils_users = { workspace = true }

View file

@ -1,4 +1,6 @@
//! Because consistency is great!
//!
pub mod uri;
pub const CONTENT_TYPE_JPG: &str = "image/jpeg";
pub const CONTENT_TYPE_PNG: &str = "image/png";
@ -6,7 +8,7 @@ pub const CONTENT_TYPE_GIF: &str = "image/gif";
pub const CONTENT_TYPE_SVG: &str = "image/svg+xml";
pub const CONTENT_TYPE_WEBP: &str = "image/webp";
// for when the user uploads things to the various image endpoints
// For when the user uploads things to the various image endpoints, these are the valid content-types.
pub const VALID_IMAGE_UPLOAD_CONTENT_TYPES: [&str; 5] = [
CONTENT_TYPE_JPG,
CONTENT_TYPE_PNG,
@ -22,13 +24,14 @@ pub const DEFAULT_CLIENT_CONFIG_PATH: &str = "/etc/kanidm/config";
/// The user-owned path for Kanidm client config
pub const DEFAULT_CLIENT_CONFIG_PATH_HOME: &str = "~/.config/kanidm";
/// The default bind address for the Kanidm server
/// The default HTTPS bind address for the Kanidm server
pub const DEFAULT_SERVER_ADDRESS: &str = "127.0.0.1:8443";
pub const DEFAULT_SERVER_LOCALHOST: &str = "localhost:8443";
/// The default LDAP bind address for the Kanidm server
pub const DEFAULT_LDAP_ADDRESS: &str = "127.0.0.1:636";
pub const DEFAULT_LDAP_LOCALHOST: &str = "localhost:636";
/// IF YOU CHANGE THESE VALUES YOU BREAK EVERYTHING
// IF YOU CHANGE THESE VALUES YOU BREAK EVERYTHING
pub const ATTR_ACCOUNT_EXPIRE: &str = "account_expire";
pub const ATTR_ACCOUNT_VALID_FROM: &str = "account_valid_from";
pub const ATTR_ACCOUNT: &str = "account";
@ -185,15 +188,18 @@ pub const LDAP_ATTR_OBJECTCLASS: &str = "objectClass";
pub const LDAP_ATTR_OU: &str = "ou";
pub const LDAP_CLASS_GROUPOFNAMES: &str = "groupofnames";
// rust can't deal with this being compiled out, don't try and #[cfg()] them
// Rust can't deal with this being compiled out, don't try and #[cfg()] them
pub const TEST_ATTR_NON_EXIST: &str = "non-exist";
pub const TEST_ATTR_TEST_ATTR: &str = "testattr";
pub const TEST_ATTR_EXTRA: &str = "extra";
pub const TEST_ATTR_NUMBER: &str = "testattrnumber";
pub const TEST_ATTR_NOTALLOWED: &str = "notallowed";
/// HTTP Header containing an auth session ID for when you're going through an auth flow
pub const KSESSIONID: &str = "X-KANIDM-AUTH-SESSION-ID";
/// HTTP Header containing the backend operation ID
pub const KOPID: &str = "X-KANIDM-OPID";
/// HTTP Header containing the Kanidm server version
pub const KVERSION: &str = "X-KANIDM-VERSION";
pub const X_FORWARDED_FOR: &str = "x-forwarded-for";

View file

@ -0,0 +1,16 @@
//! Shared URIs
//!
//! ⚠️ ⚠️ WARNING ⚠️ ⚠️
//!
//! IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS EVERYWHERE
//!
//! SERIOUSLY... DO NOT CHANGE THEM!
//!
/// ⚠️ ⚠️ WARNING DO NOT CHANGE THIS ⚠️ ⚠️
pub const OAUTH2_AUTHORISE: &str = "/oauth2/authorise";
/// ⚠️ ⚠️ WARNING DO NOT CHANGE THIS ⚠️ ⚠️
pub const OAUTH2_AUTHORISE_PERMIT: &str = "/oauth2/authorise/permit";
/// ⚠️ ⚠️ WARNING DO NOT CHANGE THIS ⚠️ ⚠️
pub const OAUTH2_AUTHORISE_REJECT: &str = "/oauth2/authorise/reject";
pub const V1_AUTH_VALID: &str = "/v1/auth/valid";

View file

@ -0,0 +1,9 @@
# OAuth Proxy Test
This dir has some things for setting up a simple OAuth2 RS so things can get tested.
## Quick Setup
1. Run the `setup_dev_environment.sh` script and set a credential for `testuser`.
2. Look for the OAuth2 Secret in the script output and copy it into a file called `client.secret` in this dir.
3. Run `./run_proxy.sh` to start the proxy, and then go to the URL and do the thing!

View file

@ -0,0 +1,12 @@
<html>
<head>
<title>OAuth Test Proxy</title>
</head>
<body>
<h1>Success!</h1>
<p>You have successfully authenticated with the OAuth proxy.</p>
<p>🦀Crabs🦀 and 🎂Cakes🎂 for everyone!</p>
</body>
</html>

View file

@ -0,0 +1,54 @@
#!/bin/bash
PROXY_VERSION="7-debian-11"
PROXY_HTTP_PORT="10080"
PROXY_HTTPS_PORT="10443"
CLIENT_ID="test_oauth2"
# documentation for proxy settings is here: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/overview/#environment-variables
# generate a cookie secret
echo "OAUTH2_PROXY_COOKIE_SECRET=$(openssl rand -hex 16)" > envfile
{
echo "OAUTH2_PROXY_CLIENT_ID=${CLIENT_ID}"
echo "OAUTH2_PROXY_CLIENT_SECRET_FILE=/opt/client.secret"
echo "OAUTH2_PROXY_COOKIE_EXPIRE=300s"
echo "OAUTH2_PROXY_CODE_CHALLENGE_METHOD=S256"
echo "OAUTH2_PROXY_COOKIE_CSRF_EXPIRE=300s"
echo "OAUTH2_PROXY_HTTP_ADDRESS=:${PROXY_HTTP_PORT}"
echo "OAUTH2_PROXY_HTTPS_ADDRESS=:${PROXY_HTTPS_PORT}"
echo "OAUTH2_PROXY_PROVIDER=oidc"
echo "OAUTH2_PROXY_SCOPE=openid"
echo "OAUTH2_PROXY_EMAIL_DOMAIN=example.com"
echo "OAUTH2_PROXY_UPSTREAM=file://opt/index.html"
echo "OAUTH2_PROXY_OIDC_ISSUER_URL=https://localhost:8443/oauth2/openid/${CLIENT_ID}"
echo "OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY=true"
# cert things, loads the certs that we use for for the test server
echo "OAUTH2_PROXY_TLS_CERT_FILE=/opt/cert.pem"
echo "OAUTH2_PROXY_TLS_KEY_FILE=/opt/key.pem"
} >> envfile
if [ ! -f client.secret ]; then
echo "The client.secret file is missing! Can't run!"
exit 1
fi
if [ -z "$(cat client.secret)" ]; then
echo "The client.secret file is empty! Can't run!"
exit 1
fi
echo "#################################################################"
echo " Starting the proxy"
echo " Access it on https://localhost:${PROXY_HTTPS_PORT}"
echo "#################################################################"
docker run --rm -it \
--env-file envfile \
--network host \
--mount "type=bind,source=/tmp/kanidm/cert.pem,target=/opt/cert.pem" \
--mount "type=bind,source=/tmp/kanidm/key.pem,target=/opt/key.pem" \
--mount "type=bind,source=./index.html,target=/opt/index.html" \
--mount "type=bind,source=./client.secret,target=/opt/client.secret" \
"bitnami/oauth2-proxy:${PROXY_VERSION}" --email-domain='*'

View file

@ -24,8 +24,12 @@ if [ "${1}" == "--help" ]; then
exit 0
fi
if [ ! -f run_insecure_dev_server.sh ]; then
echo "Please run from the server/daemon dir!"
exit 1
if [ "$(basename "$(pwd)")" == "kanidm" ]; then
cd server/daemon || exit 1
else
echo "Please run from the server/daemon dir, I can't tell where you are..."
exit 1
fi
fi
@ -115,22 +119,32 @@ ${KANIDM} group add-members "${TEST_GROUP}" "${TEST_USER_NAME}" -D idm_admin
echo "Enable experimental UI for admin idm_admin ${TEST_USER_NAME}"
${KANIDM} group add-members idm_ui_enable_experimental_features admin idm_admin "${TEST_USER_NAME}" -D idm_admin
# create oauth2 rp
echo "Creating the OAuth2 RP"
${KANIDM} system oauth2 create "${OAUTH2_RP_ID}" "${OAUTH2_RP_DISPLAY}" "https://kanidm.com" -D admin
# create oauth2 rp for kanidm.com
echo "Creating the kanidm.com OAuth2 RP"
${KANIDM} system oauth2 create "kanidm_com" "Kanidm.com" "https://kanidm.com" -D admin
echo "Creating the kanidm.com OAuth2 RP Scope Map"
${KANIDM} system oauth2 update-scope-map "kanidm_com" "${TEST_GROUP}" openid -D admin
echo "Creating the kanidm.com OAuth2 RP Supplemental Scope Map"
${KANIDM} system oauth2 update-sup-scope-map "kanidm_com" "${TEST_GROUP}" admin -D admin
echo "Creating the OAuth2 RP Scope Map"
# create oauth2 rp for localhost:10443 - for oauth2 proxy testing
echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP"
${KANIDM} system oauth2 create "${OAUTH2_RP_ID}" "${OAUTH2_RP_DISPLAY}" "https://localhost:10443" -D admin
echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP Scope Map - Group ${TEST_GROUP}"
${KANIDM} system oauth2 update-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" openid -D admin
echo "Creating the OAuth2 RP Supplemental Scope Map"
echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP Supplemental Scope Map"
${KANIDM} system oauth2 update-sup-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" admin -D admin
echo "Creating the OAuth2 RP Secondary Supplemental Crab-baite Scope Map.... wait, no that's not a thing."
echo "Checking the OAuth2 RP Exists"
${KANIDM} system oauth2 list -D admin | rg -A10 "${OAUTH2_RP_ID}"
# config auth2
echo "Pulling secret for the OAuth2 RP"
${KANIDM} system oauth2 show-basic-secret -o json "${OAUTH2_RP_ID}" -D admin
echo "Pulling secret for the ${OAUTH2_RP_ID} OAuth2 RP"
OAUTH2_SECRET="$(${KANIDM} system oauth2 show-basic-secret -o json "${OAUTH2_RP_ID}" -D admin)"
echo "${OAUTH2_SECRET}"
echo "Creating cred reset link for ${TEST_USER_NAME}"
${KANIDM} person credential create-reset-token "${TEST_USER_NAME}" -D idm_admin
@ -141,4 +155,6 @@ echo "###################################"
echo "admin password: ${ADMIN_PASS}"
echo "idm_admin password: ${IDM_ADMIN_PASS}"
echo "UI URL: ${KANIDM_URL}"
echo "OAuth2 RP ID: ${OAUTH2_RP_ID}"
echo "OAuth2 Secret: $(echo "${OAUTH2_SECRET}" | jq -r .secret)"
echo "###################################"

View file

@ -29,6 +29,7 @@ cron = { workspace = true }
filetime = { workspace = true }
futures = { workspace = true }
futures-util = { workspace = true }
hashbrown = { workspace = true }
http = { workspace = true }
hyper = { workspace = true }
kanidm_proto = { workspace = true }

View file

@ -43,15 +43,15 @@ pub struct JavaScriptFile {
impl JavaScriptFile {
/// returns a `<script>` HTML tag
pub fn as_tag(&self) -> String {
let typeattr = match &self.filetype {
let filetype = match &self.filetype {
Some(val) => {
format!(" type=\"{}\"", val.as_str())
}
_ => String::from(""),
};
format!(
r#"<script src="/pkg/{}" integrity="sha384-{}"{}></script>"#,
self.filepath, &self.hash, &typeattr,
r#"<script async src="/pkg/{}" integrity="sha384-{}"{}></script>"#,
self.filepath, &self.hash, &filetype,
)
}
}

View file

@ -20,7 +20,7 @@ pub async fn dont_cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
response
}
/// Adds `no-cache max-age=0` to the response headers.
/// Adds a cache control header of 300 seconds to the response headers.
pub async fn cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
let mut response = next.run(request).await;
let cache_header = CacheControl::new()

View file

@ -26,6 +26,7 @@ use axum::Router;
use axum_csp::{CspDirectiveType, CspValue};
use axum_macros::FromRef;
use compact_jwt::{Jws, JwsSigner, JwsUnverified};
use hashbrown::HashMap;
use http::{HeaderMap, HeaderValue};
use hyper::server::accept::Accept;
use hyper::server::conn::{AddrStream, Http};
@ -62,7 +63,7 @@ pub struct ServerState {
pub jws_signer: compact_jwt::JwsSigner,
pub jws_validator: compact_jwt::JwsValidator,
// The SHA384 hashes of javascript files we're going to serve to users
pub js_files: Vec<JavaScriptFile>,
pub js_files: JavaScriptFiles,
pub(crate) trust_x_forward_for: bool,
pub csp_header: HeaderValue,
}
@ -90,51 +91,80 @@ impl ServerState {
}
}
pub fn get_js_files(role: ServerRole) -> Vec<JavaScriptFile> {
let mut js_files: Vec<JavaScriptFile> = Vec::new();
#[derive(Clone)]
pub struct JavaScriptFiles {
all_pages: Vec<JavaScriptFile>,
selected: HashMap<String, JavaScriptFile>,
}
pub fn get_js_files(role: ServerRole) -> Result<JavaScriptFiles, ()> {
let mut all_pages: Vec<JavaScriptFile> = Vec::new();
let mut selected: HashMap<String, JavaScriptFile> = HashMap::new();
if !matches!(role, ServerRole::WriteReplicaNoUI) {
// let's set up the list of js module hashes
{
let filepath = "wasmloader.js";
for filepath in [
"wasmloader_admin.js",
"wasmloader_login_flows.js",
"wasmloader_user.js",
] {
match generate_integrity_hash(format!(
"{}/{}",
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
filepath,
)) {
Ok(hash) => js_files.push(JavaScriptFile {
filepath,
hash,
filetype: Some("module".to_string()),
}),
Ok(hash) => {
selected.insert(
filepath.to_string(),
JavaScriptFile {
filepath,
hash,
filetype: Some("module".to_string()),
},
);
}
Err(err) => {
admin_error!(?err, "Failed to generate integrity hash for wasmloader.js")
admin_error!(
?err,
"Failed to generate integrity hash for {} - cancelling startup!",
filepath
);
return Err(());
}
};
}
// let's set up the list of non-module hashes
{
let filepath = "external/bootstrap.bundle.min.js";
for (filepath, filetype) in [
("shared.js", Some("module".to_string())),
("external/bootstrap.bundle.min.js", None),
] {
// let's set up the list of non-wasm-module js files we want to serve
// for filepath in ["external/bootstrap.bundle.min.js", "shared.js"] {
match generate_integrity_hash(format!(
"{}/{}",
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
filepath,
)) {
Ok(hash) => js_files.push(JavaScriptFile {
Ok(hash) => all_pages.push(JavaScriptFile {
filepath,
hash,
filetype: None,
filetype,
}),
Err(err) => {
admin_error!(
?err,
"Failed to generate integrity hash for bootstrap.bundle.min.js"
)
"Failed to generate integrity hash for {} - cancelling startup!",
filepath
);
return Err(());
}
}
}
};
js_files
}
Ok(JavaScriptFiles {
all_pages,
selected,
})
}
pub async fn create_https_server(
@ -149,14 +179,18 @@ pub async fn create_https_server(
error!(?e, "Failed to get jws validator");
})?;
let js_files = get_js_files(config.role);
let js_files = get_js_files(config.role)?;
// set up the CSP headers
// script-src 'self'
// 'sha384-Zao7ExRXVZOJobzS/uMp0P1jtJz3TTqJU4nYXkdmsjpiVD+/wcwCyX7FGqRIqvIz'
// 'sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM'
// 'unsafe-eval';
let js_directives = js_files
.clone()
let mut all_js_files = js_files.all_pages.clone();
for (_, jsfile) in js_files.selected.clone() {
all_js_files.push(jsfile);
}
let js_directives = all_js_files
.into_iter()
.map(|f| f.hash)
.collect::<Vec<String>>();
@ -207,12 +241,18 @@ pub async fn create_https_server(
// Create a spa router that captures everything at ui without key extraction.
Router::new()
// direct users to the base app page. If a login is required,
// then views will take care of redirection. We shouldn't redir
// to login because that force clears previous sessions!
// Direct users to the base app page. If a login is required,
// then views will take care of redirection.
.route("/", get(|| async { Redirect::temporary("/ui") }))
.route("/manifest.webmanifest", get(manifest::manifest)) // skip_route_check
.nest("/ui", ui::spa_router())
// user UI app is the catch-all
.nest("/ui", ui::spa_router_user_ui())
// login flows app
.nest("/ui/login", ui::spa_router_login_flows())
.nest("/ui/reauth", ui::spa_router_login_flows())
.nest("/ui/oauth2", ui::spa_router_login_flows())
// admin app
.nest("/ui/admin", ui::spa_router_admin())
.layer(middleware::compression::new())
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
// skip_route_check
@ -238,7 +278,8 @@ pub async fn create_https_server(
}
let pkg_router = Router::new()
.nest_service("/pkg", ServeDir::new(pkg_path).precompressed_br())
.layer(middleware::compression::new());
.layer(middleware::compression::new())
.layer(from_fn(middleware::caching::cache_me));
app.merge(pkg_router)
}
};

View file

@ -14,6 +14,9 @@ use http::header::{
};
use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::Body;
use kanidm_proto::constants::uri::{
OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT, OAUTH2_AUTHORISE_REJECT,
};
use kanidm_proto::constants::APPLICATION_JSON;
use kanidm_proto::oauth2::{AccessTokenResponse, AuthorisationResponse, OidcDiscoveryResponse};
use kanidmd_lib::idm::oauth2::{
@ -742,19 +745,19 @@ pub fn route_setup(state: ServerState) -> Router<ServerState> {
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/authorise",
OAUTH2_AUTHORISE,
post(oauth2_authorise_post).get(oauth2_authorise_get),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/authorise/permit",
OAUTH2_AUTHORISE_PERMIT,
post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/authorise/reject",
OAUTH2_AUTHORISE_REJECT,
post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️

View file

@ -3,21 +3,21 @@ fn test_javscriptfile() {
// make sure it outputs what we think it does
use crate::https::JavaScriptFile;
let jsf = JavaScriptFile {
filepath: "wasmloader.js",
filepath: "wasmloader_admin.js",
hash: "1234567890".to_string(),
filetype: Some("module".to_string()),
};
assert_eq!(
jsf.as_tag(),
r#"<script src="/pkg/wasmloader.js" integrity="sha384-1234567890" type="module"></script>"#
r#"<script async src="/pkg/wasmloader_admin.js" integrity="sha384-1234567890" type="module"></script>"#
);
let jsf = JavaScriptFile {
filepath: "wasmloader.js",
filepath: "wasmloader_admin.js",
hash: "1234567890".to_string(),
filetype: None,
};
assert_eq!(
jsf.as_tag(),
r#"<script src="/pkg/wasmloader.js" integrity="sha384-1234567890"></script>"#
r#"<script async src="/pkg/wasmloader_admin.js" integrity="sha384-1234567890"></script>"#
);
}

View file

@ -8,58 +8,72 @@ use http::header::CONTENT_TYPE;
use super::middleware::KOpId;
use super::ServerState;
pub(crate) fn spa_router() -> Router<ServerState> {
pub(crate) fn spa_router_user_ui() -> Router<ServerState> {
Router::new()
.route("/", get(ui_handler))
.fallback(ui_handler)
.route("/", get(ui_handler_user_ui))
.fallback(ui_handler_user_ui)
}
pub(crate) async fn ui_handler(
/// This handles /ui/admin and all sub-paths
pub(crate) fn spa_router_admin() -> Router<ServerState> {
Router::new()
.route("/", get(ui_handler_admin))
.fallback(ui_handler_admin)
}
/// This handles the following base paths:
/// - /ui/login
/// - /ui/reauth
/// - /ui/oauth2
pub(crate) fn spa_router_login_flows() -> Router<ServerState> {
Router::new()
.route("/", get(ui_handler_login_flows))
.fallback(ui_handler_login_flows)
}
pub(crate) async fn ui_handler_user_ui(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> Response<String> {
ui_handler_generic(state, kopid, "wasmloader_user.js").await
}
pub(crate) async fn ui_handler_admin(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> Response<String> {
ui_handler_generic(state, kopid, "wasmloader_admin.js").await
}
pub(crate) async fn ui_handler_login_flows(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> Response<String> {
ui_handler_generic(state, kopid, "wasmloader_login_flows.js").await
}
pub(crate) async fn ui_handler_generic(
state: ServerState,
kopid: KOpId,
wasmloader: &str,
) -> Response<String> {
let domain_display_name = state.qe_r_ref.get_domain_display_name(kopid.eventid).await;
// 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> = state.js_files.into_iter().map(|j| j.as_tag()).collect();
let jstags = jsfiles.join(" ");
// let's get the tags we want to load the javascript files
let mut jsfiles: Vec<String> = state
.js_files
.all_pages
.into_iter()
.map(|j| j.as_tag())
.collect();
if let Some(jsfile) = state.js_files.selected.get(wasmloader) {
jsfiles.push(jsfile.clone().as_tag())
};
let jstags = jsfiles.join("\n");
let body = format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<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" />
<link rel="manifest" href="/manifest.webmanifest" />
<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-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
<link rel="stylesheet" href="/pkg/style.css"/>
{}
</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" 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>
</div>
</footer>
</body>
</html>"#,
include_str!("ui_html.html"),
domain_display_name.as_str(),
jstags,
);

View file

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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" />
<link rel="manifest" href="/manifest.webmanifest" />
<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-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" />
<link rel="stylesheet" href="/pkg/style.css" />
{}
</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"
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>
</div>
</footer>
</body>
</html>

View file

@ -9,6 +9,7 @@ use axum::routing::{delete, get, post, put};
use axum::{Extension, Json, Router};
use compact_jwt::Jws;
use http::{HeaderMap, HeaderValue};
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@ -3121,7 +3122,7 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
// get(|| async { "TODO" }),
// )
.route("/v1/auth", post(auth))
.route("/v1/auth/valid", get(auth_valid))
.route(V1_AUTH_VALID, get(auth_valid))
.route("/v1/logout", get(logout))
.route("/v1/reauth", post(reauth))
.with_state(state.clone())

View file

@ -1,7 +1,7 @@
[package]
name = "kanidmd_lib_macros"
version = "0.1.0"
edition = "2021"
edition = { workspace = true }
[lib]
proc-macro = true
@ -12,4 +12,3 @@ doctest = false
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }

View file

@ -1,7 +1,7 @@
[package]
name = "testkit-macros"
version = "0.1.0"
edition = "2021"
edition = { workspace = true }
[lib]
proc-macro = true

View file

@ -41,7 +41,7 @@ tracing = { workspace = true, features = ["attributes"] }
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
openssl = { workspace = true }
lazy_static = { workspace = true }
petgraph = { version = "0.6.4", features = ["serde"] }
petgraph = { version = "0.6.4", features = ["serde", "serde-1"] }
regex.workspace = true

View file

@ -8,13 +8,97 @@ use std::collections::{BTreeSet, HashMap};
use kanidmd_lib::constants::entries::Attribute;
use kanidmd_lib::constants::groups::{idm_builtin_admin_groups, idm_builtin_non_admin_groups};
use kanidmd_lib::prelude::{builtin_accounts, EntryInitNew};
use petgraph::graphmap::GraphMap;
use petgraph::graphmap::{AllEdges, GraphMap, NodeTrait};
use petgraph::Directed;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Deserialize, Serialize)]
enum EdgeType {
MemberOf,
}
#[derive(Debug)]
enum EntryType {
Person(String),
ServiceAccount(String),
Group(String),
UnknownType(String),
}
impl EntryType {
fn as_mermaid_tag(&self) -> String {
match self {
EntryType::Person(name) => format!("{}(\"Person:{}\")", name, name),
EntryType::ServiceAccount(name) => format!("{}{{\"SA: {}\"}}", name, name),
EntryType::Group(name) => format!("{}[\"Group: {}\"]", name, name),
EntryType::UnknownType(name) => format!("{}[\"Unknown Type {}\"]", name, name),
}
}
}
impl From<&EntryInitNew> for EntryType {
fn from(entry: &EntryInitNew) -> Self {
let name = entry.get_ava_single(Attribute::Name).unwrap();
let name = name.as_string().unwrap();
let classes = entry
.get_ava_set(Attribute::Class)
.unwrap()
.as_iutf8_set()
.cloned()
.unwrap_or(BTreeSet::<String>::new());
if classes.contains("group") {
EntryType::Group(name.clone())
} else if classes.contains("service_account") {
EntryType::ServiceAccount(name.clone())
} else if classes.contains("person") {
EntryType::Person(name.clone())
} else {
EntryType::UnknownType(name.clone())
}
}
}
struct Graph<T>(GraphMap<T, EdgeType, petgraph::Directed>);
impl<T> Graph<T>
where
T: core::hash::Hash + Ord + NodeTrait,
{
fn new() -> Self {
Graph(GraphMap::<T, EdgeType, petgraph::Directed>::new())
}
fn add_node(&mut self, n: T) {
self.0.add_node(n);
}
fn add_edge(&mut self, l: T, r: T, t: EdgeType) {
self.0.add_edge(l, r, t);
}
fn all_edges(&mut self) -> AllEdges<'_, T, EdgeType, Directed> {
self.0.all_edges()
}
/// The uuidmap is a map of uuids to EntryInitNew objects, which we use to get the name of the objects
fn as_mermaid(&mut self, uuidmap: &HashMap<T, EntryInitNew>) -> String {
let mut res = format!("graph RL;\n");
for (left, right, _weight) in self.all_edges() {
let left = uuidmap.get(&left).unwrap();
let right = uuidmap.get(&right).unwrap();
res += &format!(
" {} --> {}\n",
EntryType::from(left).as_mermaid_tag(),
EntryType::from(right).as_mermaid_tag(),
);
}
res
}
}
async fn enumerate_default_groups(/*_client: KanidmClient*/) {
let mut uuidmap: HashMap<Uuid, EntryInitNew> = HashMap::new();
let mut graph = GraphMap::<Uuid, (), petgraph::Undirected>::new();
let mut graph = Graph::new();
builtin_accounts().into_iter().for_each(|account| {
// println!("adding builtin {}", account.uuid);
@ -22,95 +106,20 @@ async fn enumerate_default_groups(/*_client: KanidmClient*/) {
graph.add_node(account.uuid);
});
idm_builtin_non_admin_groups()
.into_iter()
.for_each(|group| {
uuidmap.insert(group.uuid, group.clone().try_into().unwrap());
graph.add_node(group.uuid);
let mut groups = idm_builtin_admin_groups();
groups.extend(idm_builtin_non_admin_groups());
group.members.iter().for_each(|member| {
graph.add_edge(*member, group.uuid, ());
});
});
idm_builtin_admin_groups().into_iter().for_each(|group| {
groups.into_iter().for_each(|group| {
uuidmap.insert(group.uuid, group.clone().try_into().unwrap());
graph.add_node(group.uuid);
// handle the membership
group.members.iter().for_each(|member| {
graph.add_edge(*member, group.uuid, ());
graph.add_edge(*member, group.uuid, EdgeType::MemberOf);
});
});
// // println!("{}", mermaidchart);
// let mut dotgraph = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
// // regex to extract uuids
// // let re = regex::Regex::new(r"(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})").unwrap();
// for (uuid, uuid_value) in uuidmap.clone() {
// let uuid_str = uuid.to_string();
// if dotgraph.contains(&uuid_str) {
// // println!("uuid {} not found in graph", uuid_str);
// let name = uuid_value.get_ava_single(Attribute::Name).unwrap();
// dotgraph = dotgraph.replace(&uuid_str, name.as_string().unwrap());
// }
// }
// // println!("{}", dotgraph);
#[derive(Debug)]
enum EntryType {
Person(String),
ServiceAccount(String),
Group(String),
UnknownType(String),
}
impl EntryType {
fn as_mermaid_tag(&self) -> String {
match self {
EntryType::Person(name) => format!("{}(\"Person:{}\")", name, name),
EntryType::ServiceAccount(name) => format!("{}{{\"SA: {}\"}}", name, name),
EntryType::Group(name) => format!("{}[\"Group: {}\"]", name, name),
EntryType::UnknownType(name) => format!("{}[\"Unknown Type {}\"]", name, name),
}
}
}
impl From<EntryInitNew> for EntryType {
fn from(entry: EntryInitNew) -> Self {
let name = entry.get_ava_single(Attribute::Name).unwrap();
let name = name.as_string().unwrap();
let classes = entry
.get_ava_set(Attribute::Class)
.unwrap()
.as_iutf8_set()
.cloned()
.unwrap_or(BTreeSet::<String>::new());
if classes.contains("group") {
EntryType::Group(name.clone())
} else if classes.contains("service_account") {
EntryType::ServiceAccount(name.clone())
} else if classes.contains("person") {
EntryType::Person(name.clone())
} else {
EntryType::UnknownType(name.clone())
}
}
}
println!("graph RL;");
for (left, right, _weight) in graph.all_edges() {
let left = uuidmap.get(&left).unwrap();
// let left_name = left.get_ava_single(Attribute::Name).unwrap();
let right = uuidmap.get(&right).unwrap();
// let right_name = right.get_ava_single(Attribute::Name).unwrap();
println!(
" {} --> {}",
EntryType::from(left.clone()).as_mermaid_tag(),
EntryType::from(right.clone()).as_mermaid_tag(),
);
}
println!("{}", graph.as_mermaid(&uuidmap))
}
#[tokio::main]

View file

@ -4,6 +4,7 @@ use std::convert::TryFrom;
use std::str::FromStr;
use compact_jwt::{JwkKeySet, JwsValidator, OidcToken, OidcUnverified};
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
use kanidm_proto::constants::*;
use kanidm_proto::oauth2::{
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
@ -221,7 +222,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
let response = client
.get(rsclient.make_url("/oauth2/authorise"))
.get(rsclient.make_url(OAUTH2_AUTHORISE))
.bearer_auth(oauth_test_uat.clone())
.query(&[
("response_type", "code"),
@ -261,7 +262,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
// state and code.
let response = client
.get(rsclient.make_url("/oauth2/authorise/permit"))
.get(rsclient.make_url(OAUTH2_AUTHORISE_PERMIT))
.bearer_auth(oauth_test_uat)
.query(&[("token", consent_token.as_str())])
.send()
@ -521,7 +522,7 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
let response = client
.get(rsclient.make_url("/oauth2/authorise"))
.get(rsclient.make_url(OAUTH2_AUTHORISE))
.bearer_auth(oauth_test_uat.clone())
.query(&[
("response_type", "code"),
@ -560,7 +561,7 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
// Step 2 - we now send the consent get to the server which yields a redirect with a
// state and code.
let response = client
.get(rsclient.make_url("/oauth2/authorise/permit"))
.get(rsclient.make_url(OAUTH2_AUTHORISE_PERMIT))
.bearer_auth(oauth_test_uat)
.query(&[("token", consent_token.as_str())])
.send()

View file

@ -0,0 +1,70 @@
[package]
name = "kanidmd_web_ui_admin"
description = "Kanidm Server Web UI - Admin Interface"
documentation = "https://docs.rs/kanidm/latest/kanidm/"
version = { workspace = true }
authors = [
"William Brown <william@blackhats.net.au>",
"James Hodgkinson <james@terminaloutcomes.com>",
]
rust-version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
kanidm_proto = { workspace = true, features = ["wasm"] }
kanidmd_web_ui_shared = { workspace = true }
gloo = { workspace = true }
js-sys = { workspace = true }
lazy_static.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde-wasm-bindgen = { workspace = true }
time = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
yew = { workspace = true, features = ["csr"] }
yew-router = { workspace = true }
gloo-utils = { workspace = true }
[dependencies.web-sys]
workspace = true
features = [
# "AuthenticationExtensionsClientOutputs",
# "AuthenticatorResponse",
# "CredentialCreationOptions",
# "CredentialRequestOptions",
# "CredentialsContainer",
"DomTokenList",
"Element",
"Event",
"FocusEvent",
"FormData",
"Headers",
"HtmlButtonElement",
"HtmlDocument",
"HtmlFormElement",
"Navigator",
# "PublicKeyCredential",
# "PublicKeyCredentialCreationOptions",
# "PublicKeyCredentialRpEntity",
# "PublicKeyCredentialUserEntity",
"Request",
# "RequestCredentials",
# "RequestInit",
# "RequestMode",
# "RequestRedirect",
"Response",
"Window",
]
[dev-dependencies]
wasm-bindgen-test = { workspace = true }

View file

@ -0,0 +1,2 @@
#!/bin/sh
BUILD_FLAGS="--dev" ./build.sh

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui_admin.js';
async function main() {
await init('/pkg/kanidmd_web_ui_admin_bg.wasm');
run_app();
}
main()

View file

@ -4,14 +4,10 @@ use gloo::console;
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use super::prelude::*;
use crate::components::admin_menu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{
CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_DT, CSS_TABLE,
};
use crate::utils::{do_alert_error, do_page_header};
use crate::views::AdminRoute;
use crate::{do_request, RequestMethod};
use crate::router::AdminRoute;
use kanidmd_web_ui_shared::constants::{CSS_CELL, CSS_DT, CSS_TABLE};
impl From<GetError> for AdminListAccountsMsg {
fn from(ge: GetError) -> Self {
@ -185,11 +181,6 @@ impl Component for AdminListAccounts {
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<>
<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{"Accounts"}</li>
</ol>
{do_page_header("Account Administration")}
{ alpha_warning_banner() }
<div id={"accountlist"}>
@ -269,12 +260,12 @@ impl Component for AdminListAccounts {
console::error!("Failed to pull details", format!("{:?}", kopid));
html!(
<>
{do_alert_error("Failed to Query Accounts", Some(emsg))}
{do_alert_error("Failed to Query Accounts", Some(emsg), false)}
</>
)
}
ViewState::NotAuthorized {} => {
do_alert_error("You're not authorized to see this page!", None)
do_alert_error("You're not authorized to see this page!", None, false)
}
}}
</div>
@ -353,6 +344,7 @@ impl Component for AdminViewPerson {
ViewAccountState::Failed { emsg, kopid } => do_alert_error(
emsg.clone().as_str(),
Some(&format!("Operation ID: {:?}", kopid)),
false,
),
// TODO: the not authorized page needs to be better
ViewAccountState::NotAuthorized {} => {
@ -381,11 +373,6 @@ impl Component for AdminViewPerson {
// };
html! {
<>
<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListAccounts}>{"Accounts"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{username.as_str()}</li>
</ol>
{do_page_header(display_name.as_str())}
{alpha_warning_banner()}
@ -504,11 +491,7 @@ impl Component for AdminViewServiceAccount {
html! {
<>
<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListAccounts}>{"Accounts"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{username}</li>
</ol>
{do_page_header(&format!("Service Account: {}", username))}
{alpha_warning_banner()}
<p>{"Display Name: "}{displayname}</p>
@ -517,7 +500,7 @@ impl Component for AdminViewServiceAccount {
}
}
ViewAccountState::Failed { emsg, kopid } => html! {
do_alert_error(emsg.as_str(), Some(&format!("Operation ID: {:?}", kopid)))
do_alert_error(emsg.as_str(), Some(&format!("Operation ID: {:?}", kopid)), false)
},
// TODO: this error needs fixing
ViewAccountState::NotAuthorized {} => {

View file

@ -1,15 +1,14 @@
use std::collections::BTreeMap;
use gloo::console;
use kanidmd_web_ui_shared::utils::{do_alert_error, do_page_header};
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use crate::components::admin_menu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
use crate::utils::{do_alert_error, do_page_header};
use crate::views::AdminRoute;
use crate::{do_request, RequestMethod};
use crate::router::AdminRoute;
use kanidmd_web_ui_shared::constants::{CSS_CELL, CSS_TABLE};
use kanidmd_web_ui_shared::{alpha_warning_banner, do_request, RequestMethod};
impl From<GetError> for AdminListGroupsMsg {
fn from(ge: GetError) -> Self {
@ -224,12 +223,12 @@ impl AdminListGroups {
console::error!("Failed to pull details", format!("{:?}", kopid));
html!(
<>
{do_alert_error("Failed to Query Groups", Some(emsg))}
{do_alert_error("Failed to Query Groups", Some(emsg), false)}
</>
)
}
GroupsViewState::NotAuthorized {} => {
do_alert_error("You're not authorized to see this page!", None)
do_alert_error("You're not authorized to see this page!", None, false)
}
}
}
@ -318,11 +317,6 @@ impl Component for AdminViewGroup {
};
html! {
<>
<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListGroups}>{"Groups"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{group_name}</li>
</ol>
{do_page_header(&page_title)}
<p>{"UUID: "}{group_uuid}</p>
// TODO: pull group membership and show members
@ -337,9 +331,10 @@ impl Component for AdminViewGroup {
.as_ref()
.unwrap_or(&String::from("unknown operation ID")),
),
false,
),
GroupViewState::NotAuthorized {} => {
do_alert_error("You are not authorized to view this page!", None)
do_alert_error("You are not authorized to view this page!", None, false)
}
}
}

View file

@ -1,11 +1,13 @@
use kanidmd_web_ui_shared::alpha_warning_banner;
use serde::{Deserialize, Serialize};
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_CARD, CSS_CARD_BODY, CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER};
use kanidmd_web_ui_shared::constants::{
CSS_CARD, CSS_CARD_BODY, CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER,
};
// use crate::error::FetchError;
use crate::views::AdminRoute;
use crate::router::AdminRoute;
#[derive(Eq, PartialEq, Properties)]
pub struct Props;

View file

@ -4,12 +4,10 @@ use gloo::console;
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;
use super::prelude::*;
use crate::components::admin_menu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
use crate::utils::{do_alert_error, do_page_header};
use crate::views::AdminRoute;
use crate::{do_request, RequestMethod};
use crate::router::AdminRoute;
use kanidmd_web_ui_shared::constants::{CSS_CELL, CSS_TABLE};
impl From<GetError> for AdminListOAuth2Msg {
fn from(ge: GetError) -> Self {
@ -164,10 +162,6 @@ impl Component for AdminListOAuth2 {
html! {
<>
<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{"OAuth2"}</li>
</ol>
{do_page_header("OAuth2")}
{ alpha_warning_banner() }
@ -237,12 +231,12 @@ impl Component for AdminListOAuth2 {
console::error!("Failed to pull details", format!("{:?}", kopid));
html!(
<>
{do_alert_error("Failed to Query OAuth2", Some(emsg))}
{do_alert_error("Failed to Query OAuth2", Some(emsg), false)}
</>
)
}
ListViewState::NotAuthorized {} => {
do_alert_error("You're not authorized to see this page!", None)
do_alert_error("You're not authorized to see this page!", None, false)
}
}
}
@ -350,11 +344,6 @@ impl Component for AdminViewOAuth2 {
html! {
<>
<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListOAuth2}>{"OAuth2"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{display_name.as_str()}</li>
</ol>
{do_page_header(display_name.as_str())}
{alpha_warning_banner()}
@ -369,12 +358,12 @@ impl Component for AdminViewOAuth2 {
console::error!("Failed to pull details", format!("{:?}", kopid));
html!(
<>
{do_alert_error("Failed to Query OAuth2", Some(emsg))}
{do_alert_error("Failed to Query OAuth2", Some(emsg), false)}
</>
)
}
ViewState::NotAuthorized {} => {
do_alert_error("You're not authorized to see this page!", None)
do_alert_error("You're not authorized to see this page!", None, false)
}
}
}

View file

@ -0,0 +1,11 @@
pub mod admin_accounts;
pub mod admin_groups;
pub mod admin_menu;
pub mod admin_oauth2;
mod prelude {
pub use kanidmd_web_ui_shared::alpha_warning_banner;
pub use kanidmd_web_ui_shared::utils::{do_alert_error, do_page_header};
pub use kanidmd_web_ui_shared::{do_request, RequestMethod};
}

View file

@ -0,0 +1,152 @@
mod components;
mod router;
use gloo::console::{self, error};
use kanidmd_web_ui_shared::add_body_form_classes;
use kanidmd_web_ui_shared::constants::{
CSS_NAVBAR_BRAND, CSS_NAVBAR_LINKS_UL, CSS_NAVBAR_NAV, CSS_NAV_LINK, ID_NAVBAR_COLLAPSE,
IMG_LOGO_SQUARE, URL_USER_HOME,
};
use kanidmd_web_ui_shared::ui::{signout_link, signout_modal, ui_logout};
use kanidmd_web_ui_shared::utils::do_footer;
#[allow(unused_imports)] // because it's needed to compile wasm things
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use yew::{html, Component, Context, Html};
use yew_router::prelude::Link;
use yew_router::{BrowserRouter, Switch};
use crate::router::AdminRoute;
pub struct AdminApp {}
/// This builds the navbar, it's not generic because the link on the logo is different
fn make_navbar(links: Vec<Html>) -> Html {
html! {
<nav class={CSS_NAVBAR_NAV}>
<div class="container-fluid">
<a href={URL_USER_HOME} class={CSS_NAVBAR_BRAND}>
{"Kanidm Administration"}
</a>
// this shows a button on mobile devices to open the menu
<button class="navbar-toggler bg-light" type="button" data-bs-toggle="collapse" data-bs-target={["#", ID_NAVBAR_COLLAPSE].concat()} aria-controls={ID_NAVBAR_COLLAPSE} aria-expanded="false" aria-label="Toggle navigation">
<img src={IMG_LOGO_SQUARE} alt="Toggle navigation" class="navbar-toggler-img" />
</button>
<div class="collapse navbar-collapse" id={ID_NAVBAR_COLLAPSE}>
<ul class={CSS_NAVBAR_LINKS_UL}>
{ links.into_iter().map(|link| {
html!{ <li class="mb-1">
{ link }
</li>
} }).collect::<Html>()
}
</ul>
</div>
</div>
</nav>
}
}
#[derive(Clone, Debug)]
pub enum AdminViewsMsg {
Logout,
LogoutComplete,
}
impl Component for AdminApp {
type Message = AdminViewsMsg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)]
console::debug!("manager::create");
AdminApp {}
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
#[cfg(debug_assertions)]
console::debug!("manager::change");
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug_assertions)]
console::debug!("manager::update");
match msg {
AdminViewsMsg::Logout => {
console::debug!("manager::update -> logout");
ctx.link().send_future(async {
match Self::fetch_logout().await {
Ok(v) => v,
Err(v) => {
error!("... failed to log out? {:?}", v);
AdminViewsMsg::Logout
}
}
});
}
AdminViewsMsg::LogoutComplete => {
let window = gloo_utils::window();
window.location().set_href(URL_USER_HOME).unwrap();
}
}
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug_assertions)]
console::debug!("manager::rendered");
}
fn view(&self, ctx: &Context<Self>) -> Html {
add_body_form_classes!();
let links = vec![
html! {<a href={URL_USER_HOME} class={CSS_NAV_LINK}>{"Home"}</a>},
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>>},
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminListAccounts}>{"Accounts"}</Link<AdminRoute>>},
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminListGroups}>{"Groups"}</Link<AdminRoute>>},
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminListOAuth2}>{"OAuth2"}</Link<AdminRoute>>},
signout_link(),
];
html! {
<BrowserRouter>
// sign out modal dialogue box
{signout_modal(ctx, AdminViewsMsg::Logout)}
{make_navbar(links)}
<main class="p-3 x-auto">
<Switch<AdminRoute> render={ router::switch } />
</main>
{ do_footer() }
</BrowserRouter>
}
}
}
impl AdminApp {
async fn fetch_logout() -> Result<AdminViewsMsg, String> {
match ui_logout().await {
Ok(_) => Ok(AdminViewsMsg::LogoutComplete),
Err((emsg, _kopid)) => {
error!("failed to process logout request: {}", emsg);
Ok(AdminViewsMsg::Logout)
}
}
}
}
/// This is the entry point of the web front end. This triggers the manager app to load and begin
/// its event loop.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub fn run_app() -> Result<(), JsValue> {
yew::Renderer::<AdminApp>::new().render();
Ok(())
}

View file

@ -0,0 +1,68 @@
#![allow(clippy::disallowed_types)] // because `Routable` uses a hashmap
use serde::{Deserialize, Serialize};
use yew::{html, Html};
use yew_router::prelude::Redirect;
use yew_router::Routable;
use crate::components;
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum AdminRoute {
#[at("/ui/admin")]
AdminMenu,
#[at("/ui/admin/groups")]
AdminListGroups,
#[at("/ui/admin/accounts")]
AdminListAccounts,
#[at("/ui/admin/oauth2")]
AdminListOAuth2,
#[at("/ui/admin/group/:uuid")]
ViewGroup { uuid: String },
#[at("/ui/admin/person/:uuid")]
ViewPerson { uuid: String },
#[at("/ui/admin/service_account/:uuid")]
ViewServiceAccount { uuid: String },
#[at("/ui/admin/oauth2/:rs_name")]
ViewOAuth2RP { rs_name: String },
#[not_found]
#[at("/ui/admin/404")]
NotFound,
}
pub(crate) fn switch(route: AdminRoute) -> Html {
match route {
AdminRoute::AdminMenu => html! {
<components::admin_menu::AdminMenu />
},
AdminRoute::AdminListAccounts => html!(
<components::admin_accounts::AdminListAccounts />
),
AdminRoute::AdminListGroups => html!(
<components::admin_groups::AdminListGroups />
),
AdminRoute::AdminListOAuth2 => html!(
<components::admin_oauth2::AdminListOAuth2 />
),
AdminRoute::ViewGroup { uuid } => {
html!(<components::admin_groups::AdminViewGroup uuid={uuid} />)
// html! {<></>}
}
AdminRoute::ViewPerson { uuid } => html!(
<components::admin_accounts::AdminViewPerson uuid={uuid} />
),
AdminRoute::ViewServiceAccount { uuid } => html!(
<components::admin_accounts::AdminViewServiceAccount uuid={uuid} />
// html! {<></>}
),
AdminRoute::ViewOAuth2RP { rs_name } => html! {
<components::admin_oauth2::AdminViewOAuth2 rs_name={rs_name} />
},
AdminRoute::NotFound => html! (
<Redirect<AdminRoute> to={AdminRoute::NotFound}/>
),
}
}

View file

@ -0,0 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui_admin.js';
async function main() {
await init('/pkg/kanidmd_web_ui_admin_bg.wasm');
run_app();
}
main()

View file

@ -1,44 +1,66 @@
#!/bin/sh
set -e
# This builds the assets for the Web UI, defaulting to a release build.
# This builds the assets for the Web UI, defaulting to a release build.
if [ ! -f build_wasm.sh ]; then
echo "Please run from the crate directory. (server/web_ui)"
exit 1
fi
if [ -z "${BUILD_FLAGS}" ]; then
BUILD_FLAGS="--release --no-typescript"
fi
if [ -z "$(which rsync)" ]; then
echo "Cannot find rsync which is needed to move things around, quitting!"
exit 1
fi
if [ -z "$(which wasm-pack)" ]; then
echo "Cannot find wasm-pack which is needed to build the UI, quitting!"
exit 1
fi
if [ "$(find ./pkg/ -name 'kanidmd*' | wc -l)" -gt 0 ]; then
echo "Cleaning up WASM files before build..."
rm pkg/kanidmd*
if [ -z "${BUILD_FLAGS}" ]; then
export BUILD_FLAGS="--release"
fi
# we can disable this since we want it to expand
# shellcheck disable=SC2086
wasm-pack build ${BUILD_FLAGS} --target web --mode no-install --no-pack
echo "Cleaning up pkg dir"
find pkg/ -type f -delete
find pkg/ -mindepth 1 -type d -delete
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_ADD_TO_SRC && \
rsync --delete-after -r --copy-links -v ./static/* ./pkg/ && \
cp ../../README.md ./pkg/
cp ../../LICENSE.md ./pkg/
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_IN_BUILDS
# cp ../../README.md ./pkg/
# cp ../../LICENSE.md ./pkg/
if [ -f ./pkg/.gitignore ]; then
rm ./pkg/.gitignore
fi
# copy the shared static things
rsync -av shared/static/* shared/static/* pkg/
cd admin
echo "building admin"
../individual_build.sh || exit 1
cd ..
echo "done building admin"
cd login_flows
echo "building login_flows"
../individual_build.sh || exit 1
cd ..
echo "done building login_flows"
cd user
echo "building user"
../individual_build.sh || exit 1
cd ..
echo "done building user"
if [ -z "${SKIP_BROTLI}" ]; then
# updates the brotli-compressed files
echo "brotli-compressing the WASM file..."
find ./pkg -name '*.wasm' -exec ./find_best_brotli.sh "{}" \; || exit 1
fi
echo "brotli-compressing compressible files over 16KB in size..."
find ./pkg -size +16k -type f \
-not -name '*.br' \
-not -name '*.png' \
-exec ./find_best_brotli.sh "{}" \; || exit 1
fi

View file

@ -1 +0,0 @@
build_wasm.sh

View file

@ -14,6 +14,7 @@ if [ $# -eq 0 ]; then
fi
filename=$1
echo "#####################################"
echo "Compressing $1"
# Exit if the file doesn't exist

View file

@ -0,0 +1,45 @@
#!/bin/bash
set -e
if [ ! -f ../individual_build.sh ]; then
echo "Please run from the package base directory!"
exit 1
fi
if [ -z "${BUILD_FLAGS}" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$(which rsync)" ]; then
echo "Cannot find rsync which is needed to move things around, quitting!"
exit 1
fi
if [ -z "$(which wasm-pack)" ]; then
echo "Cannot find wasm-pack which is needed to build the UI, quitting!"
exit 1
fi
mkdir -p ./pkg
echo "Cleaning up WASM files before build..."
find ./pkg/ -name 'kanidmd*' -exec rm "{}" \;
# we can disable this since we want it to expand
# shellcheck disable=SC2086
wasm-pack build ${BUILD_FLAGS} --no-typescript --target web --mode no-install --no-pack
echo "######################"
echo "Moving files around..."
echo "######################"
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_ADD_TO_SRC && \
rm ./pkg/.gitignore
echo "######################"
echo "Moving files up into the webui pkg dir..."
echo "######################"
rsync -av pkg/* ../pkg/
echo "######################"
echo " Done!"
echo "######################"

View file

@ -0,0 +1,45 @@
[package]
name = "kanidmd_web_ui_login_flows"
description = "Kanidm Server Web UI - Login Flows"
documentation = "https://docs.rs/kanidm/latest/kanidm/"
version = { workspace = true }
authors = [
"William Brown <william@blackhats.net.au>",
"James Hodgkinson <james@terminaloutcomes.com>",
]
rust-version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
gloo = { workspace = true }
js-sys = { workspace = true }
kanidm_proto = { workspace = true, features = ["wasm"] }
kanidmd_web_ui_shared = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde-wasm-bindgen = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
yew = { workspace = true, features = ["csr"] }
yew-router = { workspace = true }
time = { workspace = true }
lazy_static.workspace = true
gloo-utils = { workspace = true }
web-sys = { workspace = true, features = [
"CredentialsContainer",
"Location",
"Navigator",
"Window",
] }
[dev-dependencies]
wasm-bindgen-test = { workspace = true }

View file

@ -0,0 +1,2 @@
#!/bin/sh
BUILD_FLAGS="--dev" ./build.sh

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui_login_flows.js';
async function main() {
await init('/pkg/kanidmd_web_ui_login_flows_bg.wasm');
run_app();
}
main()

View file

@ -1,3 +1,5 @@
//! Login flow components
// use anyhow::Error;
use gloo::console;
use kanidm_proto::v1::{
@ -5,39 +7,78 @@ use kanidm_proto::v1::{
AuthStep,
};
use kanidm_proto::webauthn::PublicKeyCredential;
use kanidmd_web_ui_shared::utils::{autofocus, do_footer, window};
use kanidmd_web_ui_shared::{
add_body_form_classes, fetch_session_valid, logo_img, remove_body_form_classes, SessionStatus,
};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::CredentialRequestOptions;
use yew::prelude::*;
use yew::virtual_dom::VNode;
use yew_router::prelude::*;
use crate::constants::{CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD};
use crate::error::FetchError;
use crate::{do_request, models, utils, RequestMethod};
use kanidmd_web_ui_shared::constants::{
CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD, CSS_ALERT_DANGER,
URL_USER_HOME,
};
use kanidmd_web_ui_shared::models::{
self, clear_bearer_token, get_bearer_token, get_login_hint, pop_login_hint,
pop_login_remember_me, pop_return_location, push_login_remember_me, set_bearer_token,
};
use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod};
use yew_router::BrowserRouter;
#[derive(Clone)]
pub struct LoginApp {
state: LoginState,
}
#[derive(Debug, PartialEq, Clone)]
impl Default for LoginApp {
fn default() -> Self {
Self {
state: LoginState::InitLogin {
enable: true,
remember_me: false,
username: String::new(),
},
}
}
}
#[derive(PartialEq, Clone, Copy)]
pub enum LoginWorkflow {
Login,
Reauth,
}
#[derive(PartialEq, Properties)]
impl std::fmt::Display for LoginWorkflow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
LoginWorkflow::Login => "LoginWorkflow::Login",
LoginWorkflow::Reauth => "LoginWorkflow::Reauth",
})
}
}
impl Default for LoginWorkflow {
fn default() -> Self {
Self::Login
}
}
#[derive(PartialEq, Properties, Default)]
pub struct LoginAppProps {
pub workflow: LoginWorkflow,
}
#[derive(PartialEq)]
#[derive(PartialEq, Clone, Copy)]
enum TotpState {
Enabled,
Disabled,
Invalid,
}
#[derive(Clone)]
enum LoginState {
InitLogin {
enable: bool,
@ -82,6 +123,7 @@ pub enum LoginAppMsg {
Select(usize),
// DoNothing,
UnknownUser,
AlreadyAuthenticated,
Error { emsg: String, kopid: Option<String> },
}
@ -94,7 +136,22 @@ impl From<FetchError> for LoginAppMsg {
}
}
impl From<SessionStatus> for LoginAppMsg {
fn from(s: SessionStatus) -> Self {
match s {
SessionStatus::TokenValid => LoginAppMsg::AlreadyAuthenticated,
SessionStatus::LoginRequired => LoginAppMsg::Begin,
SessionStatus::Error { emsg, kopid } => LoginAppMsg::Error { emsg, kopid },
}
}
}
impl LoginApp {
/// Validate that the current auth token's OK
async fn fetch_session_valid() -> Result<LoginAppMsg, FetchError> {
fetch_session_valid().await.map(|v| v.into())
}
async fn auth_init(username: String) -> Result<LoginAppMsg, FetchError> {
let authreq = AuthRequest {
step: AuthStep::Init2 {
@ -138,11 +195,11 @@ impl LoginApp {
if status == 200 {
let state: AuthResponse = serde_wasm_bindgen::from_value(value)
.expect_throw("Invalid response type - auth_init::AuthResponse");
.expect_throw("Invalid response type during reauth_init::AuthResponse");
Ok(LoginAppMsg::Next(state))
} else if status == 404 {
console::error!(format!(
"User not found: {:?}. Operation ID: {:?}",
"User not found during reauth_init: {:?}. Operation ID: {:?}",
value.as_string(),
kopid
));
@ -180,8 +237,9 @@ impl LoginApp {
fn button_start_again(&self, ctx: &Context<Self>) -> VNode {
html! {
<div class="col-md-auto text-center">
// TODO: this doesn't seem to work if you failed to login
<button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } >{" Start Again "}</button>
<button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } >
{" Start Again "}
</button>
</div>
}
}
@ -220,7 +278,7 @@ impl LoginApp {
html! {
<div class="container">
<div class="row justify-content-md-center">
<div class="alert alert-danger" role="alert">
<div class={CSS_ALERT_DANGER} role="alert">
<p><strong>{ alert_title }</strong></p>
if let Some(value) = alert_message {
<p>{ value }</p>
@ -263,6 +321,7 @@ impl LoginApp {
type="text"
autocomplete="username"
value={ username }
required=true
/>
</div>
@ -542,18 +601,21 @@ impl LoginApp {
}
}
LoginState::Authenticated => {
let loc = models::pop_return_location();
// redirect
let loc = pop_return_location();
// redirect to the "return location"
#[cfg(debug_assertions)]
console::debug!(format!("authenticated, try going to -> {:?}", loc));
loc.goto(
&ctx.link()
.navigator()
.expect_throw("failed to read history"),
);
console::debug!(format!("authenticated, trying to go to {:?}", loc));
let window = gloo_utils::window();
window
.location()
.set_href(&loc)
.expect_throw(&format!("failed to set location to {}", loc));
// this isn't likely to actually render but we might as well...
html! {
<div class="alert alert-success">
<h3>{ "Login Success 🎉" }</h3>
<a href={loc}>{"Click here to continue if you aren't redirected..."}</a>
</div>
}
}
@ -588,17 +650,48 @@ impl Component for LoginApp {
type Properties = LoginAppProps;
fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)]
console::debug!("login::create".to_string());
let workflow = ctx.props().workflow.to_owned();
#[cfg(debug_assertions)]
console::debug!(&format!("login::create -> workflow: {}", workflow));
let workflow = &ctx.props().workflow;
let state = match workflow {
LoginWorkflow::Login => {
// Assume we are here for a good reason.
// -- clear the bearer to prevent conflict
models::clear_bearer_token();
// let's check if they're already authenticated!
if get_bearer_token().is_some() {
ctx.link().send_future(async {
match Self::fetch_session_valid().await {
Ok(_) => {
console::info!(
"Already logged in, redirecting to user home page"
);
let window = gloo_utils::window();
window
.location()
.set_href(URL_USER_HOME)
.expect_throw(&["failed to set location to ", URL_USER_HOME].concat());
LoginAppMsg::AlreadyAuthenticated
}
Err(v) => {
console::error!(
"Error checking session validity, clearing token and returning to login page: {:?}",
v.as_string()
);
clear_bearer_token();
LoginAppMsg::Restart
}
}
});
}
if get_bearer_token().is_some() {
// We're already logged in, so we're going to redirect to the apps page.
return Self::default();
}
// Do we have a login hint?
let (username, remember_me) = models::get_login_hint()
let (username, remember_me) = get_login_hint()
.map(|user| (user, false))
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
.unwrap_or_default();
@ -609,18 +702,13 @@ impl Component for LoginApp {
username,
}
}
LoginWorkflow::Reauth => {
// Unlike login, don't clear tokens or cookies - these are needed during the operation
// to actually start the reauth as the same user.
match models::get_login_hint() {
Some(spn) => LoginState::InitReauth { enable: true, spn },
None => LoginState::Error {
emsg: "Client Error - No login hint available".to_string(),
kopid: None,
},
}
}
LoginWorkflow::Reauth => match get_login_hint() {
Some(spn) => LoginState::InitReauth { enable: true, spn },
None => LoginState::Error {
emsg: "Client Error - No login hint available".to_string(),
kopid: None,
},
},
};
add_body_form_classes!();
@ -634,11 +722,18 @@ impl Component for LoginApp {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
LoginAppMsg::AlreadyAuthenticated => {
#[cfg(debug_assertions)]
console::info!("User is already authenticated, redirecting to user home");
window().location().set_href(URL_USER_HOME).unwrap_throw();
// no need to render it, we're going away now
false
}
LoginAppMsg::Restart => {
// Clear any leftover input. Reset to the remembered username if any.
match &ctx.props().workflow {
LoginWorkflow::Login => {
let (username, remember_me) = models::get_login_hint()
let (username, remember_me) = get_login_hint()
.map(|user| (user, false))
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
.unwrap_or_default();
@ -650,8 +745,14 @@ impl Component for LoginApp {
};
}
LoginWorkflow::Reauth => {
match models::get_login_hint() {
Some(spn) => LoginState::InitReauth { enable: true, spn },
match get_login_hint() {
Some(spn) => {
self.state = LoginState::InitReauth {
enable: true,
spn: spn.clone(),
};
LoginState::InitReauth { enable: true, spn }
}
None => LoginState::Error {
emsg: "Client Error - No login hint available".to_string(),
kopid: None,
@ -678,10 +779,10 @@ impl Component for LoginApp {
.map(|element| element.checked())
.unwrap_or(false)
{
models::push_login_remember_me(username.clone());
push_login_remember_me(username.clone());
true
} else {
models::pop_login_remember_me();
pop_login_remember_me();
false
};
@ -711,7 +812,7 @@ impl Component for LoginApp {
}
});
self.state = match models::get_login_hint() {
self.state = match get_login_hint() {
Some(spn) => LoginState::InitReauth { enable: false, spn },
None => LoginState::Error {
emsg: "Client Error - No login hint available".to_string(),
@ -934,21 +1035,25 @@ impl Component for LoginApp {
} else {
// Else, present the options in a choice.
#[cfg(debug_assertions)]
console::debug!("multiple choices exist".to_string());
console::debug!("multiple auth method choices exist!");
self.state = LoginState::Continue(allowed);
}
true
}
AuthState::Denied(reason) => {
console::error!(format!("denied -> {:?}", reason));
console::error!(format!("Authentication denied -> {:?}", reason));
self.state = LoginState::Denied(reason);
true
}
AuthState::Success(bearer_token) => {
// Store the bearer here!
// We need to format the bearer onto it.
#[cfg(debug_assertions)]
console::info!(
"User has successfully authenticated, setting the bearer token"
);
let bearer_token = format!("Bearer {}", bearer_token);
models::set_bearer_token(bearer_token);
set_bearer_token(bearer_token);
self.state = LoginState::Authenticated;
true
}
@ -1017,40 +1122,20 @@ impl Component for LoginApp {
fn view(&self, ctx: &Context<Self>) -> Html {
#[cfg(debug_assertions)]
console::debug!("login::view".to_string());
// How do we add a top level theme?
/*
let (width, height): (u32, u32) = if let Some(win) = web_sys::window() {
let w = win.inner_width().unwrap();
let h = win.inner_height().unwrap();
ConsoleService::log(format!("width {:?} {:?}", w, w.as_f64()).as_str());
ConsoleService::log(format!("height {:?} {:?}", h, h.as_f64()).as_str());
(w.as_f64().unwrap() as u32, h.as_f64().unwrap() as u32)
} else {
ConsoleService::log("Unable to access document window");
(0, 0)
};
let (width, height) = (width.to_string(), height.to_string());
*/
// <canvas id="confetti-canvas" style="position:absolute" width=width height=height></canvas>
// May need to set these classes?
// <body class="html-body form-body">
// TODO: add the domain_display_name here
html! {
<>
<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
// More likely we should have this passed in from the props when we start.
<h3>{ "Kanidm" }</h3>
</center>
{ self.view_state(ctx) }
</main>
{ crate::utils::do_footer() }
</>
<BrowserRouter>
<main class="flex-shrink-0 form-signin">
<center>
{logo_img()}
// TODO: make a call to domain info to show the domain name
// or more likely we should have this passed in from the props when we start.
<h3>{ "Kanidm" }</h3>
</center>
// <Switch<LoginRoute> render={switch} />
{ self.view_state(ctx) }
</main>
{ do_footer() }
</BrowserRouter>
}
}
@ -1059,7 +1144,7 @@ impl Component for LoginApp {
console::debug!("login::destroy".to_string());
// Done with this, clear it.
let _ = models::pop_login_hint();
let _ = pop_login_hint();
remove_body_form_classes!();
}
@ -1068,10 +1153,10 @@ impl Component for LoginApp {
#[cfg(debug_assertions)]
console::debug!("login::rendered".to_string());
// Force autofocus on elements that need it if present.
crate::utils::autofocus("username");
crate::utils::autofocus("password");
crate::utils::autofocus("backup_code");
crate::utils::autofocus("otp");
crate::utils::autofocus("begin");
autofocus("username");
autofocus("password");
autofocus("backup_code");
autofocus("otp");
autofocus("begin");
}
}

View file

@ -0,0 +1,73 @@
//! This handles the login/auth flows, and is designed to be smol and snappy
//! so it loads fast and gets the user to where they need to go!
//!
//! - /ui/login
//! - /ui/oauth2
//! - /ui/reauth
mod components;
mod oauth2;
pub mod router;
use gloo::console;
use kanidmd_web_ui_shared::constants::URL_LOGIN;
use kanidmd_web_ui_shared::utils::window;
use router::LoginRoute;
#[allow(unused_imports)] // because it's needed to compile wasm things
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsValue, UnwrapThrowExt};
use yew::{html, Html};
use yew_router::{BrowserRouter, Switch};
use crate::components::{LoginApp, LoginWorkflow};
use crate::oauth2::Oauth2App;
// Needed for yew to pass by value
#[allow(clippy::needless_pass_by_value)]
/// Handle routes for the login_flows app
fn switch(route: LoginRoute) -> Html {
#[cfg(debug_assertions)]
console::debug!(format!("UserUiApp::switch -> {:?}", route).as_str());
match route {
LoginRoute::Login => html! {<LoginApp workflow={LoginWorkflow::Login} />},
LoginRoute::Reauth => html! {<LoginApp workflow={LoginWorkflow::Reauth} />},
LoginRoute::Oauth2 => html! {<Oauth2App />},
LoginRoute::NotFound => {
console::error!("Unknown route {}, showing login flow");
window()
.location()
.set_href(URL_LOGIN)
.expect_throw("Failed to redirect user to the login page!");
html! { <a href={URL_LOGIN}>{"Click here to return to the login page..."}</a> }
}
}
}
struct LoginFlowsApp {}
impl yew::Component for LoginFlowsApp {
type Message = ();
type Properties = ();
fn create(_ctx: &yew::Context<Self>) -> Self {
Self {}
}
fn view(&self, _ctx: &yew::Context<Self>) -> Html {
html! {
<BrowserRouter>
<Switch<LoginRoute> render={switch} />
</BrowserRouter>
}
}
}
/// This is the entry point of the web front end.
///
/// This triggers the manager app to load and begin its event loop.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub fn run_app() -> Result<(), JsValue> {
yew::Renderer::<LoginFlowsApp>::new().render();
Ok(())
}

View file

@ -1,24 +1,33 @@
use gloo::console;
use kanidm_proto::constants::APPLICATION_JSON;
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
use kanidm_proto::constants::{APPLICATION_JSON, KOPID};
pub use kanidm_proto::oauth2::{
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisationResponse,
CodeChallengeMethod, ErrorResponse,
};
use kanidmd_web_ui_shared::constants::{CONTENT_TYPE, CSS_ALERT_DANGER, URL_OAUTH2};
use kanidmd_web_ui_shared::utils::{do_alert_error, do_footer, window};
use kanidmd_web_ui_shared::{
add_body_form_classes, fetch_session_valid, logo_img, remove_body_form_classes, SessionStatus,
};
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, RequestRedirect, Response};
use yew::prelude::*;
use yew_router::prelude::*;
use crate::manager::Route;
use crate::{do_request, error::*, RequestMethod};
use crate::{models, utils};
use super::router::LoginRoute;
use kanidmd_web_ui_shared::models::{
get_bearer_token, pop_oauth2_authorisation_request, push_login_hint,
push_oauth2_authorisation_request, push_return_location,
};
use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod};
use std::collections::BTreeSet;
enum State {
LoginRequired,
// We are in the process of check the auth token to be sure we can proceed.
// We are in the process of checking the auth token to be sure we can proceed.
TokenCheck,
// Token check done, lets do it.
SubmitAuthReq,
@ -69,36 +78,32 @@ impl From<FetchError> for Oauth2Msg {
}
}
impl Oauth2App {
async fn fetch_session_valid() -> Result<Oauth2Msg, FetchError> {
let (kopid, status, value, _) =
do_request("/v1/auth/valid", RequestMethod::GET, None).await?;
if status == 200 {
Ok(Oauth2Msg::TokenValid)
} else if status == 401 {
Ok(Oauth2Msg::LoginRequired)
} else {
let emsg = value.as_string().unwrap_or_default();
// let jsval_json = JsFuture::from(resp.json()?).await?;
Ok(Oauth2Msg::Error { emsg, kopid })
impl From<SessionStatus> for Oauth2Msg {
fn from(value: SessionStatus) -> Self {
match value {
SessionStatus::TokenValid => Oauth2Msg::TokenValid,
SessionStatus::LoginRequired => Oauth2Msg::LoginRequired,
SessionStatus::Error { emsg, kopid } => Oauth2Msg::Error { emsg, kopid },
}
}
}
impl Oauth2App {
/// Validate that the current auth token's OK
async fn fetch_session_valid() -> Result<Oauth2Msg, FetchError> {
fetch_session_valid().await.map(|v| v.into())
}
async fn fetch_authreq(authreq: AuthorisationRequest) -> Result<Oauth2Msg, FetchError> {
let authreq_jsvalue = serde_json::to_string(&authreq)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq");
let (kopid, status, value, headers) = do_request(
"/oauth2/authorise",
RequestMethod::POST,
Some(authreq_jsvalue),
)
.await?;
let (kopid, status, value, headers) =
do_request(OAUTH2_AUTHORISE, RequestMethod::POST, Some(authreq_jsvalue)).await?;
#[cfg(debug_assertions)]
console::debug!(&format!("fetch_authreq {}", status));
console::debug!(&format!("fetch_authreq result {}", status));
if status == 200 {
let state: AuthorisationResponse = serde_wasm_bindgen::from_value(value)
@ -150,14 +155,14 @@ impl Oauth2App {
opts.body(Some(&consentreq_jsvalue));
let request = Request::new_with_str_and_init("/oauth2/authorise/permit", &opts)?;
let request = Request::new_with_str_and_init(OAUTH2_AUTHORISE_PERMIT, &opts)?;
request
.headers()
.set(crate::constants::CONTENT_TYPE, APPLICATION_JSON)
.set(CONTENT_TYPE, APPLICATION_JSON)
.expect_throw("failed to set header");
if let Some(bearer_token) = models::get_bearer_token() {
if let Some(bearer_token) = get_bearer_token() {
request
.headers()
.set("authorization", &bearer_token)
@ -170,7 +175,7 @@ impl Oauth2App {
let status = resp.status();
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let kopid = headers.get(KOPID).ok().flatten();
if status == 200 {
if let Some(loc) = headers.get("location").ok().flatten() {
@ -215,7 +220,7 @@ impl Component for Oauth2App {
.ok()
.or_else(|| {
console::log!("using previously storage oauth2 authorisation request if possible");
models::pop_oauth2_authorisation_request()
pop_oauth2_authorisation_request()
});
add_body_form_classes!();
@ -236,11 +241,11 @@ impl Component for Oauth2App {
// See: https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
// specifically, login_hint
if let Some(login_hint) = query.oidc_ext.login_hint.clone() {
models::push_login_hint(login_hint)
push_login_hint(login_hint)
}
// Push the request down. This covers if we move to LoginRequired so we can restore where
// we were / what we were doing.
models::push_oauth2_authorisation_request(query);
push_oauth2_authorisation_request(query);
// Start the fetch req.
// Put the fetch handle into the consent type.
@ -272,18 +277,22 @@ impl Component for Oauth2App {
true
}
Oauth2Msg::LoginProceed => {
models::push_return_location(models::Location::Manager(Route::Oauth2));
let current_loc = window()
.location()
.as_string()
.unwrap_or(URL_OAUTH2.to_string());
push_return_location(&current_loc);
ctx.link()
.navigator()
.expect_throw("failed to read history")
.push(&Route::Login);
.push(&LoginRoute::Login);
// Don't need to redraw as we are yolo-ing out.
false
}
Oauth2Msg::TokenValid => {
// Okay we can proceed, pop the query.
let ar = models::pop_oauth2_authorisation_request();
let ar = pop_oauth2_authorisation_request();
self.state = match (&self.state, ar) {
(State::TokenCheck, Some(ar)) => {
@ -402,7 +411,7 @@ impl Component for Oauth2App {
action="javascript:void(0);"
>
<h1 class="h3 mb-3 fw-normal">
// TODO: include the domain display name here
// TODO: include the domain display name here, and the RS display name?
{"Sign in to proceed" }
</h1>
<button autofocus=true class="w-100 btn btn-lg btn-primary" type="submit">
@ -476,7 +485,7 @@ impl Component for Oauth2App {
}
State::AccessDenied(kopid) => {
html! {
<div class="alert alert-danger" role="alert">
<div class={CSS_ALERT_DANGER} role="alert">
<h1>{ "Access Denied" } </h1>
<p>
{ "You do not have access to the requested resources." }
@ -492,28 +501,23 @@ impl Component for Oauth2App {
</div>
}
}
State::ErrInvalidRequest => {
html! {
<div class="alert alert-danger" role="alert">
<h1>{ "Invalid request" } </h1>
<p>
{ "Please close this window and try again again from the beginning." }
</p>
</div>
}
}
State::ErrInvalidRequest => do_alert_error(
"Invalid request",
Some("Please close this window and try again again from the beginning."),
false,
),
};
html! {
<>
<main class="form-signin">
<center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
{logo_img()}
</center>
<div class="container">
{ body_content }
</div>
</main>
{ crate::utils::do_footer() }
{ do_footer() }
</>
}
}

View file

@ -0,0 +1,18 @@
#![allow(clippy::disallowed_types)] // because `Routable` uses a hashmap
use serde::{Deserialize, Serialize};
use yew_router::Routable;
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum LoginRoute {
#[at("/ui/login")]
Login,
#[at("/ui/reauth")]
Reauth,
#[at("/ui/oauth2")]
Oauth2,
#[not_found]
#[at("/ui/login/404")]
NotFound,
}

View file

@ -0,0 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui_login_flows.js';
async function main() {
await init('/pkg/kanidmd_web_ui_login_flows_bg.wasm');
run_app();
}
main()

View file

@ -1,144 +0,0 @@
# Kanidm - Simple and Secure Identity Management
<p align="center">
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
</p>
## About
Kanidm is a simple and secure identity management platform, allowing other applications and services
to offload the challenge of authenticating and storing identities to Kanidm.
The goal of this project is to be a complete identity provider, covering the broadest possible set
of requirements and integrations. You should not need any other components (like Keycloak) when you
use Kanidm - we already have everything you need!
To achieve this we rely heavily on strict defaults, simple configuration, and self-healing
components. This allows Kanidm to run from small home labs, for families, small business, and all
the way to the largest enterprise needs.
If you want to host your own authentication service, then Kanidm is for you!
<details><summary>Supported Features</summary>
Kanidm supports:
- Webauthn (passkeys) for secure cryptographic authentication
- Oauth2/OIDC Authentication provider for web SSO
- Oauth Application Portal/Gateway allowing easy access to linked applications
- Linux/Unix integration with offline authentication
- SSH key distribution to Linux/Unix systems
- RADIUS for network and VPN authentication
- Read only LDAPS gateway for Legacy Systems
- Complete CLI tooling for Administration
- User Self Service the WebUI
</details>
## Documentation / Getting Started / Install
If you want to read more about what Kanidm can do, you should read our documentation.
- [Kanidm book (Latest stable)](https://kanidm.github.io/kanidm/stable/)
We also have a set of
[support guidelines](https://github.com/kanidm/kanidm/blob/master/project_docs/RELEASE_AND_SUPPORT.md)
for what the project team will support
## Code of Conduct / Ethics
All interactions with the project are covered by our [code of conduct].
When we develop features we follow our projects guidelines on [rights and ethics]
[code of conduct]: https://github.com/kanidm/kanidm/blob/master/CODE_OF_CONDUCT.md
[rights and ethics]: https://github.com/kanidm/kanidm/blob/master/project_docs/ethics/README.md
## Getting in Contact / Questions
We have a [gitter community channel] where project members are always happy to answer questions.
Alternately you can open a new [github discussion].
[gitter community channel]: https://gitter.im/kanidm/community
[github discussion]: https://github.com/kanidm/kanidm/discussions
## What does Kanidm mean?
Kanidm is a portmanteau of 'kani' and 'idm'. Kani is Japanese for crab, related to Rust's mascot
ferris the crab. identity management is often abbreviated to 'idm', and is a common industry term
for these services.
Kanidm is pronounced as "kar - nee - dee - em".
## Comparison with other services
<details><summary>LLDAP</summary>
[LLDAP](https://github.com/nitnelave/lldap) is a similar project aiming for a small and easy to
administer LDAP server with a web administration portal. Both projects use the
[Kanidm LDAP bindings](https://github.com/kanidm/ldap3), and have many similar ideas.
The primary benefit of Kanidm over LLDAP is that Kanidm offers a broader set of "built in" features
like Oauth2 and OIDC. To use these from LLDAP you need an external portal like Keycloak, where in
Kanidm they are "built in". However that is also a strength of LLDAP is that is offers "less" which
may make it easier to administer and deploy for you.
If Kanidm is too complex for your needs, you should check out LLDAP as a smaller alternative. If you
want a project which has a broader feature set out of the box, then Kanidm might be a better fit.
</details>
<details><summary>389-ds / OpenLDAP</summary>
Both 389-ds and OpenLDAP are generic LDAP servers. This means they only provide LDAP and you need to
bring your own IDM components - you need your own OIDC portal, webui's for self service, commandline
tools to administer and more.
If you need the highest levels of customisation possible from your LDAP deployment, then these are
probably better alternatives. If you want a service that is easy to setup and focused on IDM, then
Kanidm is a better choice.
Kanidm was originally inspired by many elements of both 389-ds and OpenLDAP. Already Kanidm is as
fast as (or faster than) 389-ds for performance and scaling as a directory service.
</details>
<details><summary>FreeIPA</summary>
FreeIPA is another identity management service for Linux/Unix, and ships a huge number of features
from LDAP, Kerberos, DNS, Certificate Authority, and more.
FreeIPA however is a complex system, with a huge amount of parts and configuration. This adds a lot
of resource overhead and difficulty for administration.
Kanidm aims to have the features richness of FreeIPA, but without the resource and administration
overheads. If you want a complete IDM package, but in a lighter footprint and easier to manage, then
Kanidm is probably for you. In testing with 3000 users + 1500 groups, Kanidm is 3 times faster for
search operations and 5 times faster for modification and addition of entries (your results may
differ however, but generally Kanidm is much faster than FreeIPA).
</details>
<details><summary>Keycloak</summary>
Keycloak is an OIDC/Oauth2/SAML provider. It allows you to layer on Webauthn to existing IDM systems.
Keycloak can operate as a stand alone IDM but generally is a component attached to an existing LDAP
server or similar.
Keycloak requires a significant amount of configuration and experience to deploy. It allows high
levels of customisation to every detail of it's authentication work flows, which makes it harder to
start with in many cases.
Kanidm does NOT require Keycloak to provide services such as Oauth2 and integrates many of the
elements in a simpler and correct way out of the box in comparison.
</details>
## Developer Getting Started
If you want to contribute to Kanidm there is a getting started [guide for developers]. IDM is a
diverse topic and we encourage contributions of many kinds in the project, from people of all
backgrounds.
When developing the server you should refer to the latest commit documentation instead.
- [Kanidm book (Latest commit)](https://kanidm.github.io/kanidm/master/)
[guide for developers]: https://kanidm.github.io/kanidm/master/DEVELOPER_README.html

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,4 @@
import { modal_hide_by_id } from '/pkg/wasmloader.js';
import { modal_hide_by_id } from '/pkg/shared.js';
let wasm;
@ -225,7 +225,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_48(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hee64a599f575a1b3(arg0, arg1);
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h29b65e42a9dd1ad6(arg0, arg1);
}
let stack_pointer = 128;
@ -237,19 +237,19 @@ function addBorrowedObject(obj) {
}
function __wbg_adapter_51(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6821a64a8b32b54e(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h17983a2b133dd53c(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}
function __wbg_adapter_54(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h578c1a08304759e7(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5882a5a28fa681a9(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_57(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb3016357f235b290(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1231c314c8a7204e(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
@ -257,7 +257,7 @@ function __wbg_adapter_57(arg0, arg1, arg2) {
/**
* This is the entry point of the web front end. This triggers the manager app to load and begin
* it's event loop.
* its event loop.
*/
export function run_app() {
try {
@ -350,22 +350,6 @@ function __wbg_get_imports() {
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = takeObject(arg0).original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret;
};
imports.wbg.__wbg_modalhidebyid_a36f33eb8222a059 = function(arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_boolean_get = function(arg0) {
const v = getObject(arg0);
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
@ -410,6 +394,19 @@ function __wbg_get_imports() {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = takeObject(arg0).original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret;
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
@ -430,6 +427,17 @@ function __wbg_get_imports() {
const ret = setInterval(getObject(arg0), arg1);
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_modalhidebyid_5fcddcb8c6ea894f = function(arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_subtreeid_e348577f7ef777e3 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_subtree_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
@ -446,14 +454,6 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setcachekey_80183b7cfc421143 = function(arg0, arg1) {
getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0;
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error();
return addHeapObject(ret);
@ -484,9 +484,6 @@ function __wbg_get_imports() {
const ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) {
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
};
imports.wbg.__wbg_getwithrefkey_5e6d9547403deab8 = function(arg0, arg1) {
const ret = getObject(arg0)[getObject(arg1)];
return addHeapObject(ret);
@ -504,16 +501,14 @@ function __wbg_get_imports() {
wasm.__wbindgen_free(arg0, arg1 * 4);
console.error(...v0);
};
imports.wbg.__wbg_log_1f7f93998ab961f7 = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.log(...v0);
};
imports.wbg.__wbg_warn_0b90a269a514ae1d = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.warn(...v0);
};
imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) {
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
};
imports.wbg.__wbg_documentURI_4bff51077cdeeac1 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).documentURI;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@ -545,44 +540,6 @@ function __wbg_get_imports() {
const ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2));
return isLikeNone(ret) ? 0 : addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Window_9029196b662bc42a = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof Window;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_document_f7ace2b956f30a4f = function(arg0) {
const ret = getObject(arg0).document;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_location_56243dba507f472d = function(arg0) {
const ret = getObject(arg0).location;
return addHeapObject(ret);
};
imports.wbg.__wbg_history_3c2280e6b2a9316e = function() { return handleError(function (arg0) {
const ret = getObject(arg0).history;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_navigator_7c9103698acde322 = function(arg0) {
const ret = getObject(arg0).navigator;
return addHeapObject(ret);
};
imports.wbg.__wbg_localStorage_dbac11bd189e9fa0 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).localStorage;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_sessionStorage_3b863b6e15dd2bdc = function() { return handleError(function (arg0) {
const ret = getObject(arg0).sessionStorage;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_fetch_336b6f0cb426b46e = function(arg0, arg1) {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Element_4622f5da1249a3eb = function(arg0) {
let result;
try {
@ -624,69 +581,43 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setAttribute_e7e80b478b7b8b2f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_state_745dc4814d321eb3 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).state;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_pushState_1145414a47c0b629 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
}, arguments) };
imports.wbg.__wbg_getItem_ed8e218e51f1efeb = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_removeItem_02359267b311cb85 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).removeItem(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_setItem_d002ee486462bfff = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_get_2e9aab260014946d = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_href_47b90f0ddf3ddcd7 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) {
imports.wbg.__wbg_instanceof_Window_9029196b662bc42a = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLInputElement;
result = getObject(arg0) instanceof Window;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_checked_5ccb3a66eb054121 = function(arg0) {
const ret = getObject(arg0).checked;
return ret;
imports.wbg.__wbg_document_f7ace2b956f30a4f = function(arg0) {
const ret = getObject(arg0).document;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_setchecked_e5a50baea447b8a8 = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
imports.wbg.__wbg_location_56243dba507f472d = function(arg0) {
const ret = getObject(arg0).location;
return addHeapObject(ret);
};
imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
imports.wbg.__wbg_history_3c2280e6b2a9316e = function() { return handleError(function (arg0) {
const ret = getObject(arg0).history;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_navigator_7c9103698acde322 = function(arg0) {
const ret = getObject(arg0).navigator;
return addHeapObject(ret);
};
imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
imports.wbg.__wbg_localStorage_dbac11bd189e9fa0 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).localStorage;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_sessionStorage_3b863b6e15dd2bdc = function() { return handleError(function (arg0) {
const ret = getObject(arg0).sessionStorage;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_fetch_336b6f0cb426b46e = function(arg0, arg1) {
const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_HtmlElement_6f4725d4677c7968 = function(arg0) {
let result;
@ -701,6 +632,48 @@ function __wbg_get_imports() {
imports.wbg.__wbg_focus_dbcbbbb2a04c0e1f = function() { return handleError(function (arg0) {
getObject(arg0).focus();
}, arguments) };
imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_sethref_e5626365d7354fea = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).href = getStringFromWasm0(arg1, arg2);
}, arguments) };
imports.wbg.__wbg_pathname_c8fd5c498079312d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).search;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).hash;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_create_c7e40b6b88186cbf = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).create(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_state_745dc4814d321eb3 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).state;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_pushState_1145414a47c0b629 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
}, arguments) };
imports.wbg.__wbg_headers_b439dcff02e808e5 = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
@ -709,59 +682,6 @@ function __wbg_get_imports() {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_log_1d3ae0273d8f4f8a = function(arg0) {
console.log(getObject(arg0));
};
imports.wbg.__wbg_newwithform_368648c82279d486 = function() { return handleError(function (arg0) {
const ret = new FormData(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_4c356dcef81d58a5 = function(arg0, arg1, arg2) {
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_credentials_66b6baa89eb03c21 = function(arg0) {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_parentNode_9e53f8b17eb98c9d = function(arg0) {
const ret = getObject(arg0).parentNode;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_parentElement_c75962bc9997ea5f = function(arg0) {
const ret = getObject(arg0).parentElement;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_lastChild_0cee692010bac6c2 = function(arg0) {
const ret = getObject(arg0).lastChild;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_nextSibling_304d9aac7c2774ae = function(arg0) {
const ret = getObject(arg0).nextSibling;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_setnodeValue_d1c8382910b45e04 = function(arg0, arg1, arg2) {
getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_textContent_c5d9e21ee03c63d4 = function(arg0, arg1) {
const ret = getObject(arg1).textContent;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_appendChild_51339d4cde00ee22 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).appendChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_insertBefore_ffa01d4b747c95fc = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_removeChild_973429f368206138 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).removeChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Response_fc4327dbfcdf5ced = function(arg0) {
let result;
try {
@ -784,78 +704,18 @@ function __wbg_get_imports() {
const ret = getObject(arg0).json();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_create_c7e40b6b88186cbf = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).create(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_e66794f89dcd7828 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).get(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_add_3eafedc4b2a28db0 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_8ae45e50cb58bb66 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_target_f171e89c61e2bccf = function(arg0) {
const ret = getObject(arg0).target;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_bubbles_63572b91f3885ef1 = function(arg0) {
const ret = getObject(arg0).bubbles;
imports.wbg.__wbg_instanceof_ShadowRoot_b64337370f59fe2d = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof ShadowRoot;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_cancelBubble_90d1c3aa2a76cbeb = function(arg0) {
const ret = getObject(arg0).cancelBubble;
return ret;
};
imports.wbg.__wbg_composedPath_cf1bb5b8bcff496f = function(arg0) {
const ret = getObject(arg0).composedPath();
return addHeapObject(ret);
};
imports.wbg.__wbg_preventDefault_24104f3f0a54546a = function(arg0) {
getObject(arg0).preventDefault();
};
imports.wbg.__wbg_addEventListener_a5963e26cd7b176b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_removeEventListener_782040b4432709cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
}, arguments) };
imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_pathname_c8fd5c498079312d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).search;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).hash;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_replace_5d1d2b7956cafd7b = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).replace(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_getClientExtensionResults_b9108fbba9f54b38 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
imports.wbg.__wbg_host_e1c47c33975060d3 = function(arg0) {
const ret = getObject(arg0).host;
return addHeapObject(ret);
};
imports.wbg.__wbg_href_17ed54b321396524 = function(arg0, arg1) {
@ -900,29 +760,62 @@ function __wbg_get_imports() {
const ret = new URL(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlFormElement_b57527983c7c1ada = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLFormElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_instanceof_ShadowRoot_b64337370f59fe2d = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof ShadowRoot;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_host_e1c47c33975060d3 = function(arg0) {
const ret = getObject(arg0).host;
imports.wbg.__wbg_newwithform_368648c82279d486 = function() { return handleError(function (arg0) {
const ret = new FormData(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_4c356dcef81d58a5 = function(arg0, arg1, arg2) {
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLInputElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_setchecked_e5a50baea447b8a8 = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
};
imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_target_f171e89c61e2bccf = function(arg0) {
const ret = getObject(arg0).target;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_bubbles_63572b91f3885ef1 = function(arg0) {
const ret = getObject(arg0).bubbles;
return ret;
};
imports.wbg.__wbg_cancelBubble_90d1c3aa2a76cbeb = function(arg0) {
const ret = getObject(arg0).cancelBubble;
return ret;
};
imports.wbg.__wbg_composedPath_cf1bb5b8bcff496f = function(arg0) {
const ret = getObject(arg0).composedPath();
return addHeapObject(ret);
};
imports.wbg.__wbg_preventDefault_24104f3f0a54546a = function(arg0) {
getObject(arg0).preventDefault();
};
imports.wbg.__wbg_href_47b90f0ddf3ddcd7 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_value_3c5f08ffc2b7d6f9 = function(arg0, arg1) {
const ret = getObject(arg1).value;
@ -934,6 +827,97 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setvalue_0dc100d4b9908028 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_getClientExtensionResults_b9108fbba9f54b38 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
return addHeapObject(ret);
};
imports.wbg.__wbg_getItem_ed8e218e51f1efeb = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_removeItem_02359267b311cb85 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).removeItem(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_setItem_d002ee486462bfff = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_addEventListener_a5963e26cd7b176b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_removeEventListener_782040b4432709cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlFormElement_b57527983c7c1ada = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLFormElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_credentials_66b6baa89eb03c21 = function(arg0) {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_parentNode_9e53f8b17eb98c9d = function(arg0) {
const ret = getObject(arg0).parentNode;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_parentElement_c75962bc9997ea5f = function(arg0) {
const ret = getObject(arg0).parentElement;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_lastChild_0cee692010bac6c2 = function(arg0) {
const ret = getObject(arg0).lastChild;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_nextSibling_304d9aac7c2774ae = function(arg0) {
const ret = getObject(arg0).nextSibling;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_setnodeValue_d1c8382910b45e04 = function(arg0, arg1, arg2) {
getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_textContent_c5d9e21ee03c63d4 = function(arg0, arg1) {
const ret = getObject(arg1).textContent;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
imports.wbg.__wbg_appendChild_51339d4cde00ee22 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).appendChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_insertBefore_ffa01d4b747c95fc = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_removeChild_973429f368206138 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).removeChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_add_3eafedc4b2a28db0 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_8ae45e50cb58bb66 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_get_2e9aab260014946d = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}, arguments) };
imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_get_44be0491f933a435 = function(arg0, arg1) {
const ret = getObject(arg0)[arg1 >>> 0];
return addHeapObject(ret);
@ -1146,20 +1130,20 @@ function __wbg_get_imports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper659 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 380, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper1151 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 587, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4175 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1999, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper3677 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1715, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4941 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2306, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper3753 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1744, __wbg_adapter_54);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5000 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2330, __wbg_adapter_57);
imports.wbg.__wbindgen_closure_wrapper3837 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1781, __wbg_adapter_57);
return addHeapObject(ret);
};
@ -1203,7 +1187,7 @@ async function __wbg_init(input) {
if (wasm !== undefined) return wasm;
if (typeof input === 'undefined') {
input = new URL('kanidmd_web_ui_bg.wasm', import.meta.url);
input = new URL('kanidmd_web_ui_user_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,6 @@
// This is easier to have in JS than in WASM
export function modal_hide_by_id(m) {
var elem = document.getElementById(m);
var modal = bootstrap.Modal.getInstance(elem);
modal.hide();
};

View file

@ -129,6 +129,13 @@ body {
right: 1rem;
}
.navbar-toggler-img {
width: 50px;
height: 50px;
padding: 0px;
margin: 0px;
}
.navbar .form-control {
padding: 0.75rem 1rem;
border-width: 0;
@ -236,3 +243,8 @@ body {
-o-transition: none !important;
transition: none !important;
}
.oauth2-img {
max-width: 150px;
max-height: 150px;
}

View file

@ -0,0 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui_admin.js';
async function main() {
await init('/pkg/kanidmd_web_ui_admin_bg.wasm');
run_app();
}
main()

View file

@ -1,7 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui.js';
import init, { run_app } from '/pkg/kanidmd_web_ui_login_flows.js';
async function main() {
await init('/pkg/kanidmd_web_ui_bg.wasm');
await init('/pkg/kanidmd_web_ui_login_flows_bg.wasm');
run_app();
}
main()

View file

@ -0,0 +1,7 @@
// loads the module which loads the WASM. It's loaders all the way down.
import init, { run_app } from '/pkg/kanidmd_web_ui_user.js';
async function main() {
await init('/pkg/kanidmd_web_ui_user_bg.wasm');
run_app();
}
main()

View file

@ -0,0 +1,68 @@
[package]
name = "kanidmd_web_ui_shared"
description = "Kanidm Server Web UI - Shared Library"
documentation = "https://docs.rs/kanidm/latest/kanidm/"
version = { workspace = true }
authors = [
"William Brown <william@blackhats.net.au>",
"James Hodgkinson <james@terminaloutcomes.com>",
]
rust-version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
# [lib]
# crate-type = ["cdylib", "rlib"]
[dependencies]
gloo = { workspace = true }
js-sys = { workspace = true }
kanidm_proto = { workspace = true, features = ["wasm"] }
lazy_static.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde-wasm-bindgen = { workspace = true }
time = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
yew = { workspace = true, features = ["csr"] }
yew-router = { workspace = true }
[dependencies.web-sys]
workspace = true
features = [
# "AuthenticationExtensionsClientOutputs",
# "AuthenticatorResponse",
# "CredentialCreationOptions",
# "CredentialRequestOptions",
# "CredentialsContainer",
"DomTokenList",
"Element",
"Event",
"FocusEvent",
"FormData",
"Headers",
"HtmlButtonElement",
"HtmlDocument",
"HtmlFormElement",
"Navigator",
# "PublicKeyCredential",
# "PublicKeyCredentialCreationOptions",
# "PublicKeyCredentialRpEntity",
# "PublicKeyCredentialUserEntity",
"Request",
# "RequestCredentials",
# "RequestInit",
# "RequestMode",
# "RequestRedirect",
"Response",
"Window",
]
[dev-dependencies]
wasm-bindgen-test = { workspace = true }

View file

@ -15,6 +15,7 @@ pub const ID_SIGNOUTMODAL: &str = "signoutModal";
pub const ID_UNIX_PASSWORDCHANGE: &str = "unixPasswordModal";
pub const ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL: &str = "identityVerificationSystemTotpModal";
pub const ID_CRED_RESET_CODE: &str = "credResetCodeModal";
pub const ID_NAVBAR_COLLAPSE: &str = "navbarCollapse";
// classes for buttons
pub const CLASS_BUTTON_DARK: &str = "btn btn-dark";
pub const CLASS_BUTTON_SUCCESS: &str = "btn btn-success";
@ -32,9 +33,29 @@ pub const CSS_CELL: &str = "p-1";
pub const CSS_DT: &str = "col-6";
pub const CSS_BREADCRUMB_ITEM: &str = "breadcrumb-item";
pub const CSS_BREADCRUMB_ITEM_ACTIVE: &str = "breadcrumb-item active";
// pub const CSS_BREADCRUMB_ITEM: &str = "breadcrumb-item";
// pub const CSS_BREADCRUMB_ITEM_ACTIVE: &str = "breadcrumb-item active";
// used in the UI for ... cards
pub const CSS_CARD: &str = "card text-center";
pub const CSS_CARD_BODY: &str = "card-body text-center";
pub const CSS_NAV_LINK: &str = "nav-link";
pub const CSS_ALERT_WARNING: &str = "alert alert-warning";
pub const CSS_ALERT_DANGER: &str = "alert alert-danger";
pub const CSS_NAVBAR_NAV: &str = "navbar navbar-expand-md navbar-dark bg-dark mb-4";
pub const CSS_NAVBAR_BRAND: &str = "navbar-brand navbar-dark";
pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav me-auto mb-2 mb-md-0";
pub const URL_ADMIN: &str = "/ui/admin";
pub const URL_OAUTH2: &str = "/ui/oauth2";
pub const URL_USER_HOME: &str = "/ui/apps";
pub const URL_USER_PROFILE: &str = "/ui/profile";
pub const URL_LOGIN: &str = "/ui/login";
pub const URL_REAUTH: &str = "/ui/reauth";
pub const URL_RESET: &str = "/ui/reset";
pub const IMG_FAVICON: &str = "/pkg/img/favicon.png";
pub const IMG_LOGO_SQUARE: &str = "/pkg/img/logo-square.svg";

View file

@ -0,0 +1,183 @@
use constants::CONTENT_TYPE;
use error::FetchError;
use gloo::console;
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use kanidm_proto::constants::KOPID;
use kanidm_proto::constants::{APPLICATION_JSON, KSESSIONID};
use models::{clear_bearer_token, get_bearer_token};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
use gloo::storage::{SessionStorage as TemporaryStorage, Storage};
use yew::{html, Html};
use crate::constants::{CSS_ALERT_WARNING, IMG_LOGO_SQUARE};
pub mod constants;
pub mod error;
#[macro_use]
pub mod macros;
pub mod models;
pub mod ui;
pub mod utils;
const AUTH_SESSION_ID: &str = "auth_session_id";
pub fn pop_auth_session_id() -> Option<String> {
let l: Result<String, _> = TemporaryStorage::get(AUTH_SESSION_ID);
#[cfg(debug_assertions)]
console::debug!(format!("auth_session_id -> {:?}", l).as_str());
TemporaryStorage::delete(AUTH_SESSION_ID);
l.ok()
}
pub fn push_auth_session_id(r: String) {
TemporaryStorage::set(AUTH_SESSION_ID, r).expect_throw(&format!(
"failed to set {} in temporary storage",
AUTH_SESSION_ID
));
}
/// Build and send a request to the backend, with some standard headers and pull back
/// (kopid, status, json, headers)
pub async fn do_request(
uri: &str,
method: RequestMethod,
body: Option<JsValue>,
) -> Result<(Option<String>, u16, JsValue, Headers), FetchError> {
let mut opts = RequestInit::new();
opts.method(&method.to_string());
opts.mode(RequestMode::SameOrigin);
opts.credentials(web_sys::RequestCredentials::SameOrigin);
if let Some(body) = body {
#[cfg(debug_assertions)]
if method == RequestMethod::GET {
gloo::console::debug!("This seems odd, you've supplied a body with a GET request?")
}
opts.body(Some(&body));
}
let request = Request::new_with_str_and_init(uri, &opts)?;
request
.headers()
.set(CONTENT_TYPE, APPLICATION_JSON)
.expect_throw("failed to set content-type header");
if let Some(sessionid) = pop_auth_session_id() {
request
.headers()
.set(KSESSIONID, &sessionid)
.expect_throw(&format!("failed to set {} header", KSESSIONID));
}
if let Some(bearer_token) = get_bearer_token() {
request
.headers()
.set("authorization", &bearer_token)
.expect_throw("failed to set authorization header");
}
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
let headers: Headers = resp.headers();
if let Some(sessionid) = headers.get(KSESSIONID).ok().flatten() {
push_auth_session_id(sessionid);
}
let kopid = headers.get(KOPID).ok().flatten();
let body = match resp.json() {
Ok(json_future) => match JsFuture::from(json_future).await {
Ok(body) => body,
Err(e) => {
let e_msg = format!("future json error -> {:?}", e);
console::error!(e_msg.as_str());
JsValue::NULL
}
},
Err(e) => {
let e_msg = format!("response json error -> {:?}", e);
console::error!(e_msg.as_str());
JsValue::NULL
}
};
Ok((kopid, status, body, headers))
}
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum RequestMethod {
GET,
POST,
PUT,
}
impl ToString for RequestMethod {
fn to_string(&self) -> String {
match self {
RequestMethod::PUT => "PUT".to_string(),
RequestMethod::POST => "POST".to_string(),
RequestMethod::GET => "GET".to_string(),
}
}
}
/// creates the "Kanidm is alpha" banner
pub fn alpha_warning_banner() -> Html {
html!(
<div class={CSS_ALERT_WARNING} role="alert">
{"🦀 Kanidm is still in early Alpha, this interface is a placeholder! "}
</div>
)
}
/// Returns a HTML img tag with the Kanidm logo
pub fn logo_img() -> Html {
html! {
<img src={IMG_LOGO_SQUARE} alt="Kanidm" class="kanidm_logo"/>
}
}
pub enum SessionStatus {
TokenValid,
LoginRequired,
Error { emsg: String, kopid: Option<String> },
}
impl ToString for SessionStatus {
fn to_string(&self) -> String {
match self {
SessionStatus::TokenValid => "SessionStatus::TokenValid".to_string(),
SessionStatus::LoginRequired => "SessionStatus::LoginRequired".to_string(),
SessionStatus::Error { emsg, kopid } => {
format!("SessionStatus::Error: {} {:?}", emsg, kopid)
}
}
}
}
/// Validate that the current stored session token is valid
pub async fn fetch_session_valid() -> Result<SessionStatus, FetchError> {
let (kopid, status, value, _) = do_request(V1_AUTH_VALID, RequestMethod::GET, None).await?;
if status == 200 {
Ok(SessionStatus::TokenValid)
} else if status == 401 {
#[cfg(debug_assertions)]
console::debug!("Session token is invalid, clearing it");
clear_bearer_token();
Ok(SessionStatus::LoginRequired)
} else {
let emsg = value.as_string().unwrap_or_default();
Ok(SessionStatus::Error { emsg, kopid })
}
}

View file

@ -0,0 +1,120 @@
#[cfg(debug_assertions)]
use gloo::console;
use gloo::storage::{LocalStorage, SessionStorage as TemporaryStorage, Storage};
use kanidm_proto::oauth2::AuthorisationRequest;
use kanidm_proto::v1::{CUSessionToken, CUStatus};
use wasm_bindgen::UnwrapThrowExt;
use crate::constants::URL_USER_HOME;
const BEARER_TOKEN: &str = "bearer_token";
const CRED_UPDATE_SESSION: &str = "cred_update_session";
const LOGIN_HINT: &str = "login_hint";
const LOGIN_REMEMBER_ME: &str = "login_remember_me";
const RETURN_LOCATION: &str = "return_location";
const OAUTH2_AUTHORIZATION_REQUEST: &str = "oauth2_authorisation_request";
/// Store the bearer token `r` in local storage
pub fn set_bearer_token(r: String) {
LocalStorage::set(BEARER_TOKEN, r).expect_throw(&format!("failed to set {}", BEARER_TOKEN));
}
pub fn get_bearer_token() -> Option<String> {
let l: Result<String, _> = LocalStorage::get(BEARER_TOKEN);
#[cfg(debug_assertions)]
console::debug!(&format!(
"login_hint::get_login_remember_me -> present={:?}",
l.is_ok()
));
l.ok()
}
pub fn clear_bearer_token() {
#[cfg(debug_assertions)]
console::debug!("clearing the bearer token from local storage");
LocalStorage::delete(BEARER_TOKEN);
}
/// Keep the "return location" in temporary storage when we're planning to do a redirect
pub fn push_return_location(l: &str) {
TemporaryStorage::set(RETURN_LOCATION, l).expect_throw(&format!(
"failed to set {} in temporary storage",
RETURN_LOCATION
));
}
/// We keep the "return location" in temporary storage when we're planning to do a redirect,
/// this pulls it back and removes it from storage.
pub fn pop_return_location() -> String {
let l: Result<String, _> = TemporaryStorage::get(RETURN_LOCATION);
#[cfg(debug_assertions)]
console::debug!(format!("{} -> {:?}", RETURN_LOCATION, l).as_str());
TemporaryStorage::delete(RETURN_LOCATION);
l.unwrap_or(URL_USER_HOME.to_string())
}
/// Store the user's username in temporary storage when we're passing it around.
pub fn push_login_hint(username: String) {
TemporaryStorage::set(LOGIN_HINT, username).expect_throw("failed to set login hint");
}
pub fn get_login_hint() -> Option<String> {
let l: Result<String, _> = TemporaryStorage::get(LOGIN_HINT);
#[cfg(debug_assertions)]
console::debug!(format!("login_hint::get_login_hint -> {:?}", l).as_str());
l.ok()
}
pub fn pop_login_hint() -> Option<String> {
let l: Result<String, _> = TemporaryStorage::get(LOGIN_HINT);
#[cfg(debug_assertions)]
console::debug!(format!("login_hint::pop_login_hint -> {:?}", l).as_str());
TemporaryStorage::delete(LOGIN_HINT);
l.ok()
}
/// Keep track of the user's username when they set the "remember me" flag on the UI
pub fn push_login_remember_me(username: String) {
LocalStorage::set(LOGIN_REMEMBER_ME, username).expect_throw("failed to set login remember me");
}
pub fn get_login_remember_me() -> Option<String> {
let username: Result<String, _> = LocalStorage::get(LOGIN_REMEMBER_ME);
#[cfg(debug_assertions)]
console::debug!(format!("login_hint::get_login_remember_me -> {:?}", username).as_str());
username.ok()
}
pub fn pop_login_remember_me() -> Option<String> {
let username: Result<String, _> = LocalStorage::get(LOGIN_REMEMBER_ME);
#[cfg(debug_assertions)]
console::debug!(format!("login_hint::pop_login_remember_me -> {:?}", username).as_str());
LocalStorage::delete(LOGIN_REMEMBER_ME);
username.ok()
}
pub fn push_oauth2_authorisation_request(r: AuthorisationRequest) {
TemporaryStorage::set(OAUTH2_AUTHORIZATION_REQUEST, r).expect_throw(&format!(
"failed to set {} in temporary storage",
OAUTH2_AUTHORIZATION_REQUEST
));
}
pub fn pop_oauth2_authorisation_request() -> Option<AuthorisationRequest> {
let l: Result<AuthorisationRequest, _> = TemporaryStorage::get(OAUTH2_AUTHORIZATION_REQUEST);
#[cfg(debug_assertions)]
console::debug!(format!("{} -> {:?}", OAUTH2_AUTHORIZATION_REQUEST, l).as_str());
TemporaryStorage::delete(OAUTH2_AUTHORIZATION_REQUEST);
l.ok()
}
/// Pushes the "cred_update_session" element into the browser's temporary storage
pub fn push_cred_update_session(s: (CUSessionToken, CUStatus)) {
TemporaryStorage::set(CRED_UPDATE_SESSION, s).expect_throw("failed to set cred session token");
}
/// Pulls the "cred_update_session" element from the browser's temporary storage
pub fn get_cred_update_session() -> Option<(CUSessionToken, CUStatus)> {
let l: Result<(CUSessionToken, CUStatus), _> = TemporaryStorage::get(CRED_UPDATE_SESSION);
l.ok()
}

View file

@ -0,0 +1,70 @@
//! UI things
//!
use gloo::console;
use yew::{html, BaseComponent, Context, Html};
use crate::constants::{CSS_NAV_LINK, ID_SIGNOUTMODAL};
use crate::models::clear_bearer_token;
use crate::{do_request, RequestMethod};
/// returns an a-href link which can trigger the signout flow
pub fn signout_link() -> Html {
html! {
<a class={CSS_NAV_LINK} href="#" data-bs-toggle="modal"
data-bs-target={["#", ID_SIGNOUTMODAL].concat()}
>{"Sign out"}</a>
}
}
/// does the logout action, calling the api and clearing the local tokens
pub async fn ui_logout() -> Result<(), (String, Option<String>)> {
let (kopid, status, value, _) = do_request("/v1/logout", RequestMethod::GET, None)
.await
.map_err(|e| {
let emsg = format!("failed to logout -> {:?}", e);
console::error!(emsg.as_str());
(emsg, None)
})?;
if status == 200 {
// only clear the local token if it actually worked, because otherwise you could
// think the session is gone, while it's still live.
clear_bearer_token();
Ok(())
} else {
let emsg = value.as_string().unwrap_or_default();
Err((emsg, kopid))
}
}
/// Builds the signout modal dialogue box - the "target" is the Message to send when clicked.
pub fn signout_modal<T, U>(ctx: &Context<T>, target: U) -> Html
where
T: BaseComponent,
U: Clone + 'static,
<T as BaseComponent>::Message: From<U>,
{
html! {<div class="modal" tabindex="-1" role="dialog" id={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={["#", ID_SIGNOUTMODAL].concat()}
onclick={ ctx.link().callback(move |_| target.clone()) }>{ "Sign out" }</button>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal"
>{"Cancel"}</button>
</div>
</div>
</div>
</div>}
}

View file

@ -1,4 +1,5 @@
use gloo::console;
use url::Url;
use wasm_bindgen::prelude::*;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
@ -7,6 +8,8 @@ use web_sys::{Document, HtmlElement, HtmlInputElement, Window};
use yew::virtual_dom::VNode;
use yew::{html, Html};
use crate::constants::{CSS_ALERT_DANGER, CSS_PAGE_HEADER};
pub fn window() -> Window {
web_sys::window().expect_throw("Unable to retrieve window")
}
@ -77,7 +80,7 @@ pub fn get_value_from_element_id(id: &str) -> Option<String> {
.map(|element| element.value())
}
#[wasm_bindgen(raw_module = "/pkg/wasmloader.js")]
#[wasm_bindgen(raw_module = "/pkg/shared.js")]
extern "C" {
pub fn modal_hide_by_id(m: &str);
}
@ -93,15 +96,18 @@ pub fn do_footer() -> VNode {
}
}
pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>) -> Html {
pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>, dismissable: bool) -> Html {
html! {
<div class="container">
<div class="row justify-content-md-center">
<div class="alert alert-danger" role="alert">
<div class={CSS_ALERT_DANGER} role="alert">
<p><strong>{ alert_title }</strong></p>
if let Some(value) = alert_message {
<p>{ value }</p>
}
if dismissable {
<button type="button" class="btn btn-close" data-dismiss="alert" aria-label="Close"></button>
}
</div>
</div>
</div>
@ -110,7 +116,7 @@ pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>) -> Html {
pub fn do_page_header(page_title: &str) -> Html {
html! {
<div class={crate::constants::CSS_PAGE_HEADER}>
<div class={CSS_PAGE_HEADER}>
<h2>{ page_title }</h2>
</div>
}

Some files were not shown because too many files have changed in this diff Show more