Improve badlist updating ()

This commit is contained in:
Firstyear 2022-10-07 11:35:58 +10:00 committed by GitHub
parent 47ef3697c9
commit e9ed430199
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 375 additions and 243 deletions

90
Cargo.lock generated
View file

@ -158,7 +158,7 @@ dependencies = [
"num-traits",
"rusticata-macros",
"thiserror",
"time 0.3.14",
"time 0.3.15",
]
[[package]]
@ -846,13 +846,13 @@ dependencies = [
[[package]]
name = "console"
version = "0.15.1"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"once_cell",
"terminal_size",
"unicode-width",
"winapi",
@ -904,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
dependencies = [
"percent-encoding",
"time 0.3.14",
"time 0.3.15",
"version_check",
]
@ -920,7 +920,7 @@ dependencies = [
"publicsuffix",
"serde",
"serde_json",
"time 0.3.14",
"time 0.3.15",
"url",
]
@ -1037,15 +1037,14 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.10"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1"
checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"crossbeam-utils",
"memoffset",
"once_cell",
"scopeguard",
]
@ -1061,12 +1060,11 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.11"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
dependencies = [
"cfg-if 1.0.0",
"once_cell",
]
[[package]]
@ -1247,9 +1245,9 @@ dependencies = [
[[package]]
name = "devd-rs"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c315b8fe6f26aea3091b030c28aabbdf491376ae39033978f06e468ab42360c"
checksum = "9313f104b590510b46fc01c0a324fc76505c13871454d3c48490468d04c8d395"
dependencies = [
"libc",
"nom 7.1.1",
@ -1522,6 +1520,17 @@ dependencies = [
"futures-sink",
]
[[package]]
name = "futures-concurrency"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49242554e83bfb20ec3fa39db8fbdd349ed7c905efd39dfb9b83a517f41b05b1"
dependencies = [
"async-trait",
"futures-core",
"pin-project",
]
[[package]]
name = "futures-core"
version = "0.3.24"
@ -2257,11 +2266,11 @@ dependencies = [
"clap_complete",
"compact_jwt",
"dialoguer",
"futures-concurrency",
"kanidm_client",
"kanidm_proto",
"libc",
"qrcode",
"rayon",
"rpassword 7.0.0",
"serde",
"serde_json",
@ -2290,7 +2299,7 @@ dependencies = [
"kanidmd_lib",
"libc",
"libsqlite3-sys",
"lru 0.8.0",
"lru 0.8.1",
"profiles",
"r2d2",
"r2d2_sqlite",
@ -2414,7 +2423,7 @@ dependencies = [
"kanidm_proto",
"qrcode",
"serde",
"serde-wasm-bindgen 0.4.3",
"serde-wasm-bindgen 0.4.5",
"serde_json",
"uuid",
"wasm-bindgen",
@ -2488,9 +2497,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.133"
version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]]
name = "libgit2-sys"
@ -2612,9 +2621,9 @@ dependencies = [
[[package]]
name = "lru"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936d98d2ddd79c18641c6709e7bb09981449694e402d1a0f0f657ea8d61f4a51"
checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909"
dependencies = [
"hashbrown",
]
@ -3239,9 +3248,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.44"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [
"unicode-ident",
]
@ -3772,9 +3781,9 @@ dependencies = [
[[package]]
name = "serde-wasm-bindgen"
version = "0.4.3"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfc62771e7b829b517cb213419236475f434fb480eddd76112ae182d274434a"
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
dependencies = [
"js-sys",
"serde",
@ -3999,9 +4008,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
dependencies = [
"serde",
]
@ -4209,18 +4218,18 @@ checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
[[package]]
name = "thiserror"
version = "1.0.36"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.36"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
"proc-macro2",
"quote",
@ -4291,9 +4300,9 @@ dependencies = [
[[package]]
name = "tikv-jemalloc-sys"
version = "0.5.1+5.3.0-patched"
version = "0.5.2+5.3.0-patched"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "931e876f91fed0827f863a2d153897790da0b24d882c721a79cb3beb0b903261"
checksum = "ec45c14da997d0925c7835883e4d5c181f196fa142f8c19d7643d1e9af2592c3"
dependencies = [
"cc",
"fs_extra",
@ -4339,9 +4348,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.14"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
dependencies = [
"itoa 1.0.3",
"libc",
@ -4405,9 +4414,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.21.1"
version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
dependencies = [
"autocfg",
"bytes",
@ -4415,7 +4424,6 @@ dependencies = [
"memchr",
"mio",
"num_cpus",
"once_cell",
"pin-project-lite 0.2.9",
"signal-hook-registry",
"socket2",
@ -4967,7 +4975,7 @@ dependencies = [
"base64urlsafedata",
"js-sys",
"serde",
"serde-wasm-bindgen 0.4.3",
"serde-wasm-bindgen 0.4.5",
"serde_json",
"url",
"wasm-bindgen",
@ -5111,7 +5119,7 @@ dependencies = [
"oid-registry",
"rusticata-macros",
"thiserror",
"time 0.3.14",
"time 0.3.15",
]
[[package]]
@ -5231,5 +5239,5 @@ dependencies = [
"lazy_static",
"quick-error",
"regex",
"time 0.3.14",
"time 0.3.15",
]

View file

@ -59,6 +59,7 @@ dyn-clone = "^1.0.9"
fernet = "^0.2.0"
filetime = "^0.2.17"
futures = "^0.3.21"
futures-concurrency = "^3.0.0"
futures-util = "^0.3.21"
gloo = "^0.8.0"
gloo-net = "0.2.4"
@ -92,8 +93,6 @@ qrcode = "^0.12.0"
r2d2 = "^0.8.9"
r2d2_sqlite = "^0.21.0"
rand = "^0.8.5"
# try to remove this
rayon = "^1.5.3"
regex = "1.5.6"
reqwest = "0.11.11"
rpassword = "^7.0.0"

View file

@ -1,38 +1,52 @@
# Password Quality and Badlisting
Kanidm embeds a set of tools to help your users use and create strong passwords.
This is important as not all user types will require multi-factor authentication (MFA)
for their roles, but compromised accounts still pose a risk. There may also be deployment
Kanidm embeds a set of tools to help your users use and create strong passwords.
This is important as not all user types will require multi-factor authentication (MFA)
for their roles, but compromised accounts still pose a risk. There may also be deployment
or other barriers to a site rolling out sitewide MFA.
## Quality Checking
Kanidm enforces that all passwords are checked by the library "[zxcvbn](https://github.com/dropbox/zxcvbn)".
This has a large number of checks for password quality. It also provides constructive feedback to users on how
Kanidm enforces that all passwords are checked by the library "[zxcvbn](https://github.com/dropbox/zxcvbn)".
This has a large number of checks for password quality. It also provides constructive feedback to users on how
to improve their passwords if they are rejected.
Some things that zxcvbn looks for is use of the account name or email in the password, common passwords,
Some things that zxcvbn looks for is use of the account name or email in the password, common passwords,
low entropy passwords, dates, reverse words and more.
This library can not be disabled - all passwords in Kanidm must pass this check.
## Password Badlisting
This is the process of configuring a list of passwords to exclude from being able to be used.
This is especially useful if a specific business has been notified of a compromised account, allowing
This is the process of configuring a list of passwords to exclude from being able to be used.
This is especially useful if a specific business has been notified of compromised accounts, allowing
you to maintain a list of customised excluded passwords.
The other value to this feature is being able to badlist common passwords that zxcvbn does not detect, or
The other value to this feature is being able to badlist common passwords that zxcvbn does not detect, or
from other large scale password compromises.
By default we ship with a preconfigured badlist that is updated over time as new password breach lists are
By default we ship with a preconfigured badlist that is updated over time as new password breach lists are
made available.
## Updating your own Badlist
The password badlist by default is append only, meaning it can only grow, but will never remove
passwords previously considered breached.
You can update your own badlist by using the provided `kanidm_badlist_preprocess` tool which helps to automate this process.
### Updating your own Badlist
Given a list of passwords in a text file, it will generate a modification set which can be applied.
The tool also provides the command you need to run to apply this:
You can display the current badlist with:
kanidm system pw-badlist show
You can update your own badlist with:
kanidm system pw-badlist upload "path/to/badlist" [...]
Multiple bad lists can be listed and uploaded at once. These are preprocessed to identify and remove
passwords that zxcvbn and our password rules would already have eliminated. That helps to make the bad
list more efficent to operate over at run time.
## Password Rotation
Kanidm will never support this "anti-feature". Password rotation encourages poor password hygiene and
is not shown to prevent any attacks.
kanidm_badlist_preprocess -m -o /tmp/modlist.json <password file> [<password file> <password file> ...]

View file

@ -39,6 +39,7 @@ use webauthn_rs_proto::{
mod person;
mod service_account;
mod system;
pub const APPLICATION_JSON: &str = "application/json";
pub const KOPID: &str = "X-KANIDM-OPID";

View file

@ -0,0 +1,26 @@
use crate::{ClientError, KanidmClient};
impl KanidmClient {
pub async fn system_password_badlist_get(&self) -> Result<Vec<String>, ClientError> {
let list: Option<Vec<String>> = self
.perform_get_request("/v1/system/_attr/badlist_password")
.await?;
Ok(list.unwrap_or_default())
}
pub async fn system_password_badlist_append(
&self,
list: Vec<String>,
) -> Result<(), ClientError> {
self.perform_post_request("/v1/system/_attr/badlist_password", list)
.await
}
pub async fn system_password_badlist_remove(
&self,
list: Vec<String>,
) -> Result<(), ClientError> {
self.perform_delete_request_with_body("/v1/system/_attr/badlist_password", list)
.await
}
}

View file

@ -25,19 +25,15 @@ doc = false
name = "kanidm_ssh_authorizedkeys_direct"
path = "src/ssh_authorizedkeys.rs"
[[bin]]
name = "kanidm_badlist_preprocess"
path = "src/badlist_preprocess.rs"
[dependencies]
clap = { workspace = true, features = ["derive", "env"] }
compact_jwt.workspace = true
dialoguer.workspace = true
futures-concurrency.workspace = true
libc.workspace = true
kanidm_client.workspace = true
kanidm_proto.workspace = true
qrcode = { workspace = true, default-features = false }
rayon.workspace = true
rpassword.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true

View file

@ -8,7 +8,6 @@ use clap_complete::{generate_to, Shell};
use uuid::Uuid;
include!("src/opt/ssh_authorizedkeys.rs");
include!("src/opt/badlist_preprocess.rs");
include!("src/opt/kanidm.rs");
fn main() {
@ -42,21 +41,6 @@ fn main() {
)
.ok();
generate_to(
Shell::Bash,
&mut BadlistProcOpt::command(),
"kanidm_badlist_preprocess",
comp_dir.clone(),
)
.ok();
generate_to(
Shell::Zsh,
&mut BadlistProcOpt::command(),
"kanidm_badlist_preprocess",
comp_dir.clone(),
)
.ok();
generate_to(
Shell::Bash,
&mut KanidmClientParser::command(),

View file

@ -1,144 +0,0 @@
#![deny(warnings)]
#![warn(unused_extern_crates)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
use std::fs::File;
use std::io::prelude::*;
use std::io::BufWriter;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use clap::Parser;
use kanidm_proto::v1::Modify;
use rayon::prelude::*;
use tracing::{debug, error, info};
include!("opt/badlist_preprocess.rs");
fn main() {
let opt = BadlistProcOpt::parse();
if opt.debug {
::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug");
}
tracing_subscriber::fmt::init();
if opt.modlist {
debug!("Running in modlist generation mode");
} else {
debug!("Running in list filtering mode");
}
info!("Kanidm badlist preprocessor - this may take a long time ...");
// We open the file early to find out if we can create it or not.
let fileout = match File::create(opt.outfile) {
Ok(f) => f,
Err(e) => {
error!("Failed to create file - {:?}", e);
return;
}
};
// Build a temp struct for all the pws.
// Shellexpand all of these.
/*
let expanded_paths: Vec<_> = opt.password_list.iter()
.map(|p| {
shellexpand::tilde(p).into_owned()
})
.collect();
debug!("Using paths -> {:?}", expanded_paths);
*/
let mut pwset: Vec<String> = Vec::new();
// Read them all in, remove blank lines.
for f in opt.password_list.iter() {
let mut file = match File::open(f) {
Ok(v) => v,
Err(_) => {
info!("Skipping file -> {:?}", f);
continue;
}
};
let mut contents = String::new();
if let Err(e) = file.read_to_string(&mut contents) {
error!("{:?} -> {:?}", f, e);
continue;
}
let mut inner_pw: Vec<_> = contents.as_str().lines().map(str::to_string).collect();
pwset.append(&mut inner_pw);
}
debug!("Deduplicating pre-set ...");
pwset.sort_unstable();
pwset.dedup();
info!("Have {} pws to process", pwset.len());
let count: AtomicUsize = AtomicUsize::new(0);
// Create an empty slice for empty site options, not needed in this context.
let site_opts: Vec<&str> = Vec::new();
// Run zxcbvn over them with filter, use btreeset to remove dups if any
let mut filt_pwset: Vec<_> = pwset
.into_par_iter()
.inspect(|_| {
let tc = count.fetch_add(1, Ordering::AcqRel);
if tc % 1000 == 0 {
info!("{} ...", tc)
}
})
.filter(|v| {
if v.is_empty() {
return false;
}
if v.len() < 10 {
return false;
}
match zxcvbn::zxcvbn(v.as_str(), site_opts.as_slice()) {
// score of 2 or less is too weak and we'd already reject it.
Ok(r) => r.score() >= 3,
Err(e) => {
error!("zxcvbn unable to process '{}' - {:?}", v.as_str(), e);
error!("adding to badlist anyway ...");
true
}
}
})
.collect();
// Now sort and dedup
debug!("Deduplicating results ...");
filt_pwset.sort_unstable();
filt_pwset.dedup();
debug!("Starting file write ...");
// Now we write these out.
let bwrite = BufWriter::new(fileout);
// All remaining are either
if opt.modlist {
// - written to a file ready for modify, with a modify command printed.
let modlist: Vec<Modify> = filt_pwset
.into_iter()
.map(|p| Modify::Present("badlist_password".to_string(), p))
.collect();
match serde_json::to_writer(bwrite, &modlist) {
Ok(_) =>
info!("next step: kanidm raw modify -D admin '{{\"Eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffff000026\"]}}' <outfile>"),
Err(e) => {
error!("Failed to serialised modifications - {:?}", e)
}
}
} else {
// - printed in json format
if let Err(e) = serde_json::to_writer_pretty(bwrite, &filt_pwset) {
error!("Failed to serialised badlist - {:?}", e)
}
}
}

View file

@ -0,0 +1,161 @@
use crate::PwBadlistOpt;
use futures_concurrency::prelude::*;
// use std::thread;
use std::fs::File;
use std::io::Read;
use tokio::task;
const CHUNK_SIZE: usize = 1000;
impl PwBadlistOpt {
pub fn debug(&self) -> bool {
match self {
PwBadlistOpt::Show(copt) => copt.debug,
PwBadlistOpt::Upload { copt, .. } => copt.debug,
PwBadlistOpt::Remove { copt, .. } => copt.debug,
}
}
pub async fn exec(&self) {
match self {
PwBadlistOpt::Show(copt) => {
let client = copt.to_client().await;
match client.system_password_badlist_get().await {
Ok(list) => {
for i in list {
println!("{}", i);
}
eprintln!("--");
eprintln!("Success");
}
Err(e) => eprintln!("{:?}", e),
}
}
PwBadlistOpt::Upload { copt, paths } => {
let client = copt.to_client().await;
info!("pre-processing - this may take a while ...");
let mut pwset: Vec<String> = Vec::new();
for f in paths.iter() {
let mut file = match File::open(f) {
Ok(v) => v,
Err(e) => {
debug!(?e);
info!("Skipping file -> {:?}", f);
continue;
}
};
let mut contents = String::new();
if let Err(e) = file.read_to_string(&mut contents) {
error!("{:?} -> {:?}", f, e);
continue;
}
let mut inner_pw: Vec<_> =
contents.as_str().lines().map(str::to_string).collect();
pwset.append(&mut inner_pw);
}
debug!("Deduplicating pre-set ...");
pwset.sort_unstable();
pwset.dedup();
info!("Have {} unique passwords to process", pwset.len());
// Break the list into chunks per thread availability
// let par_count = thread::available_parallelism()
// .expect("Failed to determine available parallelism")
// .get();
let task_handles: Vec<_> = pwset
.chunks(CHUNK_SIZE)
.map(|chunk| chunk.to_vec())
.map(|chunk| {
task::spawn_blocking(move || {
let x = chunk
.iter()
.filter(|v| {
if v.len() < 10 {
return false;
}
match zxcvbn::zxcvbn(v.as_str(), &[]) {
Ok(r) => r.score() >= 4,
Err(e) => {
error!(
"zxcvbn unable to process '{}' - {:?}",
v.as_str(),
e
);
error!("adding to badlist anyway ...");
true
}
}
})
.map(|s| s.to_string())
.collect::<Vec<_>>();
eprint!(".");
x
})
})
.collect();
let results = task_handles.join().await;
let results: Vec<_> = results
.into_iter()
.map(|res| res.expect("Thread join failure"))
.collect();
let filt_pwset: Vec<String> = results.into_iter().flatten().collect();
info!(
"{} passwords passed zxcvbn, uploading ...",
filt_pwset.len()
);
match client.system_password_badlist_append(filt_pwset).await {
Ok(_) => println!("Success"),
Err(e) => eprintln!("{:?}", e),
}
} // End Upload
PwBadlistOpt::Remove { copt, paths } => {
let client = copt.to_client().await;
let mut pwset: Vec<String> = Vec::new();
for f in paths.iter() {
let mut file = match File::open(f) {
Ok(v) => v,
Err(e) => {
debug!(?e);
info!("Skipping file -> {:?}", f);
continue;
}
};
let mut contents = String::new();
if let Err(e) = file.read_to_string(&mut contents) {
error!("{:?} -> {:?}", f, e);
continue;
}
let mut inner_pw: Vec<_> =
contents.as_str().lines().map(str::to_string).collect();
pwset.append(&mut inner_pw);
}
debug!("Deduplicating pre-set ...");
pwset.sort_unstable();
pwset.dedup();
if pwset.is_empty() {
eprintln!("No entries to remove?");
return;
}
match client.system_password_badlist_remove(pwset).await {
Ok(_) => println!("Success"),
Err(e) => eprintln!("{:?}", e),
}
} // End Remove
}
}
}

View file

@ -20,6 +20,7 @@ use uuid::Uuid;
include!("../opt/kanidm.rs");
pub mod badlist;
pub mod common;
pub mod domain;
pub mod group;
@ -64,6 +65,7 @@ impl SelfOpt {
impl SystemOpt {
pub fn debug(&self) -> bool {
match self {
SystemOpt::PwBadlist { commands } => commands.debug(),
SystemOpt::Oauth2 { commands } => commands.debug(),
SystemOpt::Domain { commands } => commands.debug(),
}
@ -71,6 +73,7 @@ impl SystemOpt {
pub async fn exec(&self) {
match self {
SystemOpt::PwBadlist { commands } => commands.exec().await,
SystemOpt::Oauth2 { commands } => commands.exec().await,
SystemOpt::Domain { commands } => commands.exec().await,
}

View file

@ -13,11 +13,12 @@
use clap::Parser;
use kanidm_cli::KanidmClientParser;
use std::thread;
use tokio::runtime;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
#[tokio::main(flavor = "current_thread")]
async fn main() {
fn main() {
let opt = KanidmClientParser::parse();
let fmt_layer = fmt::layer().with_writer(std::io::stderr);
@ -43,5 +44,18 @@ async fn main() {
.with(fmt_layer)
.init();
opt.commands.exec().await
let par_count = thread::available_parallelism()
.expect("Failed to determine available parallelism")
.get();
let rt = runtime::Builder::new_current_thread()
// We configure this as it's used by the badlist pre-processor
.max_blocking_threads(par_count)
.enable_all()
.build()
.expect("Failed to initialise tokio runtime!");
tracing::debug!("Using {} worker threads", par_count);
rt.block_on(async { opt.commands.exec().await });
}

View file

@ -1,11 +0,0 @@
#[derive(Debug, Parser)]
struct BadlistProcOpt {
#[clap(short, long)]
debug: bool,
#[clap(short, long)]
modlist: bool,
#[clap(short, long = "output")]
outfile: PathBuf,
#[clap(parse(from_os_str))]
password_list: Vec<PathBuf>,
}

View file

@ -643,6 +643,33 @@ pub struct OptSetDomainDisplayName {
new_display_name: String,
}
#[derive(Debug, Subcommand)]
pub enum PwBadlistOpt {
#[clap[name = "show"]]
/// Show information about this system's password badlist
Show(CommonOpt),
#[clap[name = "upload"]]
/// Upload an extra badlist, appending to the currently configured one.
/// This badlist will be preprocessed to remove items that are already
/// caught by "zxcvbn" at the configured level.
Upload {
#[clap(flatten)]
copt: CommonOpt,
#[clap(parse(from_os_str))]
paths: Vec<PathBuf>,
},
#[clap[name = "remove", hide = true]]
/// Remove the content of these lists if present in the configured
/// badlist.
Remove {
#[clap(flatten)]
copt: CommonOpt,
#[clap(parse(from_os_str))]
paths: Vec<PathBuf>,
}
}
#[derive(Debug, Subcommand)]
pub enum DomainOpt {
#[clap[name = "set_domain_display_name"]]
@ -659,6 +686,12 @@ pub enum DomainOpt {
#[derive(Debug, Subcommand)]
pub enum SystemOpt {
#[clap(name = "pw-badlist")]
/// Configure and manage the password badlist entry
PwBadlist {
#[clap(subcommand)]
commands: PwBadlistOpt,
},
#[clap(name = "oauth2")]
/// Configure and display oauth2/oidc resource server configuration
Oauth2 {
@ -719,6 +752,7 @@ pub enum KanidmClientOpt {
commands: RecycleOpt,
},
/// Unsafe - low level, raw database queries and operations.
#[clap(hide = true)]
Raw {
#[clap(subcommand)]
commands: RawOpt,

View file

@ -791,6 +791,14 @@ pub fn create_https_server(
.mapped_put(&mut routemap, domain_put_attr)
.mapped_delete(&mut routemap, domain_delete_attr);
let mut system_route = appserver.at("/v1/system");
system_route.at("/").mapped_get(&mut routemap, system_get);
system_route
.at("/_attr/:attr")
.mapped_get(&mut routemap, system_get_attr)
.mapped_post(&mut routemap, system_post_attr)
.mapped_delete(&mut routemap, system_delete_attr);
let mut recycle_route = appserver.at("/v1/recycle_bin");
recycle_route
.at("/")

View file

@ -204,6 +204,24 @@ pub async fn json_rest_event_put_attr(
to_tide_response(res, hvalue)
}
pub async fn json_rest_event_post_attr(
mut req: tide::Request<AppState>,
uuid_or_name: String,
filter: Filter<FilterInvalid>,
) -> tide::Result {
let uat = req.get_current_uat();
let attr = req.get_url_param("attr")?;
let values: Vec<String> = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_appendattribute(uat, uuid_or_name, attr, values, filter, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn json_rest_event_put_id_attr(
req: tide::Request<AppState>,
filter: Filter<FilterInvalid>,
@ -893,6 +911,27 @@ pub async fn domain_delete_attr(req: tide::Request<AppState>) -> tide::Result {
json_rest_event_delete_attr(req, filter, STR_UUID_DOMAIN_INFO.to_string(), attr).await
}
pub async fn system_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("uuid", PartialValue::new_uuid(UUID_SYSTEM_CONFIG)));
json_rest_event_get(req, filter, None).await
}
pub async fn system_get_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("system_config")));
json_rest_event_get_attr(req, STR_UUID_SYSTEM_CONFIG, filter).await
}
pub async fn system_post_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("system_config")));
json_rest_event_post_attr(req, STR_UUID_SYSTEM_CONFIG.to_string(), filter).await
}
pub async fn system_delete_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("system_config")));
let attr = req.get_url_param("attr")?;
json_rest_event_delete_attr(req, filter, STR_UUID_SYSTEM_CONFIG.to_string(), attr).await
}
pub async fn recycle_bin_get(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_pres("class"));
let uat = req.get_current_uat();

View file

@ -1022,6 +1022,9 @@ pub const JSON_IDM_ACP_SYSTEM_CONFIG_PRIV_V1: &str = r#"{
"description",
"badlist_password"
],
"acp_modify_removedattr": [
"badlist_password"
],
"acp_modify_presentattr": [
"badlist_password"
]

View file

@ -753,12 +753,9 @@ pub trait QueryServerTransaction<'a> {
// This is a helper to get password badlist.
fn get_password_badlist(&self) -> Result<HashSet<String>, OperationError> {
self.internal_search_uuid(&UUID_SYSTEM_CONFIG)
.and_then(|e| match e.get_ava_iter_iutf8("badlist_password") {
Some(vs_str_iter) => {
let badlist_hashset: HashSet<_> = vs_str_iter.map(str::to_string).collect();
Ok(badlist_hashset)
}
None => Err(OperationError::InvalidEntryState),
.map(|e| match e.get_ava_iter_iutf8("badlist_password") {
Some(vs_str_iter) => vs_str_iter.map(str::to_string).collect::<HashSet<_>>(),
None => HashSet::default(),
})
.map_err(|e| {
admin_error!(?e, "Failed to retrieve system configuration");