mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-20 16:03:55 +02:00
Improve badlist updating (#1095)
This commit is contained in:
parent
47ef3697c9
commit
e9ed430199
90
Cargo.lock
generated
90
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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> ...]
|
||||
|
|
|
@ -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";
|
||||
|
|
26
kanidm_client/src/system.rs
Normal file
26
kanidm_client/src/system.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
161
kanidm_tools/src/cli/badlist.rs
Normal file
161
kanidm_tools/src/cli/badlist.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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("/")
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue