diff --git a/.gitignore b/.gitignore index 060c0a3fa..baedabbff 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ test.db /vendor kanidm_rlm_python/test_data/certs/ +vendor.tar.gz + diff --git a/kanidm_book/src/SUMMARY.md b/kanidm_book/src/SUMMARY.md index 31f6e9b9d..93671216c 100644 --- a/kanidm_book/src/SUMMARY.md +++ b/kanidm_book/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Accounts and Groups](./accounts_and_groups.md) - [SSH Key Distribution](./ssh_key_dist.md) - [RADIUS](./radius.md) +- [Password Quality and Badlisting](./password_quality.md) ----------- [Why TLS?](./why_tls.md) diff --git a/kanidm_book/src/password_quality.md b/kanidm_book/src/password_quality.md new file mode 100644 index 000000000..764615cfb --- /dev/null +++ b/kanidm_book/src/password_quality.md @@ -0,0 +1,40 @@ +# 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 MFA for their roles, but compromised accounts still pose a risk. +There may also be deployment or other barriers to a site rolling out site wide MFA. + +## Quality Checking + +Kanidm enforces that all passwords are checked by the library "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 it was rejected. + +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 +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 from other large scale password compromises. + +By default we ship with a preconfigured badlist that is updated overtime as new password breach +lists are made available. + +## Updating your own badlist. + +You can update your own badlist by using the proided `kanidm_badlist_preprocess` tool which helps +to automate this process. + +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. + + kanidm_badlist_preprocess -m -o /tmp/modlist.json [ ...] + + diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index ba47e3801..8c197a69b 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -258,13 +258,19 @@ fn test_server_admin_reset_simple_password() { let res = rsclient.modify(f.clone(), m.clone()); assert!(res.is_ok()); - // Now set it's password. + // Now set it's password - should be rejected based on low quality let res = rsclient.idm_account_primary_credential_set_password("testperson", "password"); + assert!(res.is_err()); + // Set the password to ensure it's good + let res = rsclient.idm_account_primary_credential_set_password( + "testperson", + "tai4eCohtae9aegheo3Uw0oobahVighaig6heeli", + ); assert!(res.is_ok()); // Check it stuck. let tclient = rsclient.new_session().expect("failed to build new session"); assert!(tclient - .auth_simple_password("testperson", "password") + .auth_simple_password("testperson", "tai4eCohtae9aegheo3Uw0oobahVighaig6heeli") .is_ok()); // Generate a pw instead diff --git a/kanidm_proto/Cargo.toml b/kanidm_proto/Cargo.toml index 38c422609..1176bbca0 100644 --- a/kanidm_proto/Cargo.toml +++ b/kanidm_proto/Cargo.toml @@ -9,6 +9,8 @@ serde = "1.0" serde_derive = "1.0" uuid = { version = "0.7", features = ["serde", "v4"] } actix = { version = "0.7", optional = true } +zxcvbn = { version = "2.0", features = ["ser"] } [dev-dependencies] serde_json = "1.0" + diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index aa4839c48..78d0e9f8f 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use std::fmt; use uuid::Uuid; +// use zxcvbn::feedback; // These proto implementations are here because they have public definitions @@ -25,6 +26,22 @@ pub enum PluginError { } #[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum ConsistencyError { + Unknown, + // Class, Attribute + SchemaClassMissingAttribute(String, String), + QueryServerSearchFailure, + EntryUuidCorrupt(u64), + UuidIndexCorrupt(String), + UuidNotUnique(String), + RefintNotUpheld(u64), + MemberOfInvalid(u64), + InvalidAttributeType(String), + DuplicateUniqueAttribute(String), + InvalidSPN(u64), +} + +#[derive(Serialize, Deserialize, Debug)] pub enum OperationError { EmptyRequest, Backend, @@ -57,22 +74,19 @@ pub enum OperationError { InvalidSessionState, SystemProtectedObject, SystemProtectedAttribute, + PasswordTooWeak, + PasswordTooShort(usize), + PasswordEmpty, + PasswordBadListed, } -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub enum ConsistencyError { - Unknown, - // Class, Attribute - SchemaClassMissingAttribute(String, String), - QueryServerSearchFailure, - EntryUuidCorrupt(u64), - UuidIndexCorrupt(String), - UuidNotUnique(String), - RefintNotUpheld(u64), - MemberOfInvalid(u64), - InvalidAttributeType(String), - DuplicateUniqueAttribute(String), - InvalidSPN(u64), +impl PartialEq for OperationError { + fn eq(&self, other: &Self) -> bool { + // We do this to avoid InvalidPassword being checked as it's not + // derive PartialEq. Generally we only use the PartialEq for TESTING + // anyway. + std::mem::discriminant(self) == std::mem::discriminant(other) + } } /* ===== higher level types ===== */ diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index 1b39d5310..8c6c0e1a3 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -13,6 +13,10 @@ path = "src/main.rs" name = "kanidm_ssh_authorizedkeys" path = "src/ssh_authorizedkeys.rs" +[[bin]] +name = "kanidm_badlist_preprocess" +path = "src/badlist_preprocess.rs" + [dependencies] kanidm_client = { path = "../kanidm_client" } kanidm_proto = { path = "../kanidm_proto" } @@ -23,4 +27,6 @@ env_logger = "0.6" serde = "1.0" serde_json = "1.0" shellexpand = "1.0" +rayon = "1.2" +zxcvbn = "2.0" diff --git a/kanidm_tools/src/badlist_preprocess.rs b/kanidm_tools/src/badlist_preprocess.rs new file mode 100644 index 000000000..a176e89ed --- /dev/null +++ b/kanidm_tools/src/badlist_preprocess.rs @@ -0,0 +1,136 @@ +extern crate structopt; + +// use shellexpand; +use rayon::prelude::*; +use serde_json; +use std::fs::File; +use std::io::prelude::*; +use std::io::BufWriter; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use structopt::StructOpt; +use zxcvbn; + +use kanidm_proto::v1::Modify; + +extern crate env_logger; +#[macro_use] +extern crate log; + +#[derive(Debug, StructOpt)] +struct ClientOpt { + #[structopt(short = "d", long = "debug")] + debug: bool, + #[structopt(short = "m", long = "modlist")] + modlist: bool, + #[structopt(short = "o", long = "output")] + outfile: PathBuf, + #[structopt(parse(from_os_str))] + password_list: Vec, +} + +fn main() { + let opt = ClientOpt::from_args(); + if opt.debug { + ::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug"); + } else { + ::std::env::set_var("RUST_LOG", "kanidm=info,kanidm_client=info"); + } + env_logger::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 ..."); + + // Build a temp struct for all the pws. + // TODO: 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 = 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(); + match file.read_to_string(&mut contents) { + Ok(_) => {} + Err(e) => { + error!("{:?} -> {:?}", f, e); + continue; + } + } + let mut inner_pw: Vec<_> = contents.as_str().lines().map(|s| s.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.len() == 0 { + return false; + } + if v.len() < 10 { + return false; + } + let r = zxcvbn::zxcvbn(v.as_str(), site_opts.as_slice()).expect("Empty Password?"); + // score of 2 or less is too weak and we'd already reject it. + r.score() >= 3 + }) + .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 fileout = File::create(opt.outfile).expect("Failed to create file"); + 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 = filt_pwset + .into_iter() + .map(|p| Modify::Present("badlist_password".to_string(), p)) + .collect(); + serde_json::to_writer_pretty(bwrite, &modlist).expect("Failed to serialise modlist"); + // println!("next step: kanidm raw modify -D admin '{{\"Eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffff000026\"]}}' "); + } else { + // - printed in json format + serde_json::to_writer_pretty(bwrite, &filt_pwset).expect("Failed to serialise modlist"); + } +} diff --git a/kanidmd/Cargo.toml b/kanidmd/Cargo.toml index 539d16d8c..9a6d8283c 100644 --- a/kanidmd/Cargo.toml +++ b/kanidmd/Cargo.toml @@ -57,4 +57,5 @@ rpassword = "0.4" num_cpus = "1.10" idlset = "0.1" +zxcvbn = "2.0" diff --git a/kanidmd/src/lib/constants.rs b/kanidmd/src/lib/constants/mod.rs similarity index 95% rename from kanidmd/src/lib/constants.rs rename to kanidmd/src/lib/constants/mod.rs index 80865d8f7..5dbc08e20 100644 --- a/kanidmd/src/lib/constants.rs +++ b/kanidmd/src/lib/constants/mod.rs @@ -1,5 +1,9 @@ use uuid::Uuid; +// Re-export as needed +pub mod system_config; +pub use crate::constants::system_config::JSON_SYSTEM_CONFIG_V1; + // Increment this as we add new schema types and values!!! pub static SYSTEM_INDEX_VERSION: i64 = 3; // On test builds, define to 60 seconds @@ -10,6 +14,7 @@ pub static PURGE_TIMEOUT: u64 = 60; pub static PURGE_TIMEOUT: u64 = 3600; // 5 minute auth session window. pub static AUTH_SESSION_TIMEOUT: u64 = 300; +pub static PW_MIN_LENGTH: usize = 10; // Built in group and account ranges. pub static STR_UUID_ADMIN: &'static str = "00000000-0000-0000-0000-000000000000"; @@ -108,11 +113,14 @@ pub static UUID_SCHEMA_ATTR_DOMAIN_SSID: &'static str = "00000000-0000-0000-0000 pub static UUID_SCHEMA_ATTR_GIDNUMBER: &'static str = "00000000-0000-0000-0000-ffff00000056"; pub static UUID_SCHEMA_CLASS_POSIXACCOUNT: &'static str = "00000000-0000-0000-0000-ffff00000057"; pub static UUID_SCHEMA_CLASS_POSIXGROUP: &'static str = "00000000-0000-0000-0000-ffff00000058"; +pub static UUID_SCHEMA_ATTR_BADLIST_PASSWORD: &'static str = "00000000-0000-0000-0000-ffff00000059"; +pub static UUID_SCHEMA_CLASS_SYSTEM_CONFIG: &'static str = "00000000-0000-0000-0000-ffff00000060"; // System and domain infos // I'd like to strongly criticise william of the past for fucking up these allocations. pub static _UUID_SYSTEM_INFO: &'static str = "00000000-0000-0000-0000-ffffff000001"; pub static UUID_DOMAIN_INFO: &'static str = "00000000-0000-0000-0000-ffffff000025"; +// DO NOT allocate here, allocate below. // Access controls // skip 00 / 01 - see system info @@ -154,6 +162,9 @@ pub static _UUID_IDM_ACP_HP_GROUP_MANAGE_PRIV_V1: &'static str = "00000000-0000-0000-0000-ffffff000024"; // Skip 25 - see domain info. pub static UUID_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &'static str = "00000000-0000-0000-0000-ffffff000026"; +pub static STR_UUID_SYSTEM_CONFIG: &'static str = "00000000-0000-0000-0000-ffffff000027"; +pub static UUID_IDM_ACP_SYSTEM_CONFIG_PRIV_V1: &'static str = + "00000000-0000-0000-0000-ffffff000028"; // End of system ranges pub static STR_UUID_DOES_NOT_EXIST: &'static str = "00000000-0000-0000-0000-fffffffffffe"; @@ -163,6 +174,7 @@ lazy_static! { pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap(); pub static ref UUID_DOES_NOT_EXIST: Uuid = Uuid::parse_str(STR_UUID_DOES_NOT_EXIST).unwrap(); pub static ref UUID_ANONYMOUS: Uuid = Uuid::parse_str(STR_UUID_ANONYMOUS).unwrap(); + pub static ref UUID_SYSTEM_CONFIG: Uuid = Uuid::parse_str(STR_UUID_SYSTEM_CONFIG).unwrap(); } pub static JSON_ADMIN_V1: &'static str = r#"{ @@ -1204,7 +1216,7 @@ pub static JSON_IDM_ACP_HP_GROUP_MANAGE_PRIV_V1: &'static str = r#"{ } }"#; -// 25 - domain admins acp +// 28 - domain admins acp pub static JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &'static str = r#"{ "attrs": { "class": [ @@ -1238,6 +1250,36 @@ pub static JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &'static str = r#"{ } }"#; +// 28 - system config +pub static JSON_IDM_ACP_SYSTEM_CONFIG_PRIV_V1: &'static str = r#"{ + "attrs": { + "class": [ + "object", + "access_control_profile", + "access_control_search", + "access_control_modify" + ], + "name": ["idm_acp_system_config_priv"], + "uuid": ["00000000-0000-0000-0000-ffffff000028"], + "description": ["Builtin IDM Control for granting system configuration rights"], + "acp_receiver": [ + "{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000019\"]}" + ], + "acp_targetscope": [ + "{\"And\": [{\"Eq\": [\"uuid\",\"00000000-0000-0000-0000-ffffff000027\"]}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}" + ], + "acp_search_attr": [ + "name", + "uuid", + "description", + "badlist_password" + ], + "acp_modify_presentattr": [ + "badlist_password" + ] + } +}"#; + // Anonymous should be the last opbject in the range here. pub static JSON_ANONYMOUS_V1: &'static str = r#"{ "attrs": { @@ -1538,6 +1580,7 @@ pub static JSON_SCHEMA_ATTR_DOMAIN_SSID: &'static str = r#"{ ] } }"#; + pub static JSON_SCHEMA_ATTR_GIDNUMBER: &'static str = r#"{ "attrs": { "class": [ @@ -1567,6 +1610,35 @@ pub static JSON_SCHEMA_ATTR_GIDNUMBER: &'static str = r#"{ } }"#; +pub static JSON_SCHEMA_ATTR_BADLIST_PASSWORD: &'static str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "A password that is badlisted meaning that it can not be set as a valid password by any user account." + ], + "index": [], + "unique": [ + "true" + ], + "multivalue": [ + "true" + ], + "attributename": [ + "badlist_password" + ], + "syntax": [ + "UTF8STRING_INSENSITIVE" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000059" + ] + } +}"#; + pub static JSON_SCHEMA_CLASS_PERSON: &'static str = r#" { "valid": { @@ -1719,6 +1791,7 @@ pub static JSON_SCHEMA_CLASS_POSIXGROUP: &'static str = r#" } } "#; + pub static JSON_SCHEMA_CLASS_POSIXACCOUNT: &'static str = r#" { "attrs": { @@ -1743,6 +1816,31 @@ pub static JSON_SCHEMA_CLASS_POSIXACCOUNT: &'static str = r#" } "#; +pub static JSON_SCHEMA_CLASS_SYSTEM_CONFIG: &'static str = r#" + { + "attrs": { + "class": [ + "object", + "system", + "classtype" + ], + "description": [ + "The class representing a system (topologies) configuration options." + ], + "classname": [ + "system_config" + ], + "systemmay": [ + "description", + "badlist_password" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000060" + ] + } + } +"#; + // need a domain_trust_info as well. // TODO diff --git a/kanidmd/src/lib/constants/rockyou_3_10.json b/kanidmd/src/lib/constants/rockyou_3_10.json new file mode 100644 index 000000000..d67ad99c0 --- /dev/null +++ b/kanidmd/src/lib/constants/rockyou_3_10.json @@ -0,0 +1,638 @@ +[ + "100preteamare", + "14defebrero", + "1life1love", + "1life2live", + "1love1life", + "1love4life", + "212224236248", + "2813308004", + "2fast2furious", + "2gether4ever", + "2pacshakur", + "30secondstomars", + "3doorsdown", + "6cyclemind", + "
// account expiry? (as opposed to cred expiry) + pub spn: String, + // TODO: When you add mail, you should update the check to zxcvbn + // to include these. + // pub mail: Vec } impl Account { @@ -170,17 +185,13 @@ impl Account { #[cfg(test)] mod tests { use crate::constants::JSON_ANONYMOUS_V1; - use crate::entry::{Entry, EntryNew, EntryValid}; - use crate::idm::account::Account; + // use crate::entry::{Entry, EntryNew, EntryValid}; + // use crate::idm::account::Account; #[test] fn test_idm_account_from_anonymous() { - let anon_e: Entry = - unsafe { Entry::unsafe_from_entry_str(JSON_ANONYMOUS_V1).to_valid_new() }; - let anon_e = unsafe { anon_e.to_valid_committed() }; - - let anon_account = Account::try_from_entry_no_groups(anon_e).expect("Must not fail"); - println!("{:?}", anon_account); + let anon_e = entry_str_to_account!(JSON_ANONYMOUS_V1); + println!("{:?}", anon_e); // I think that's it? we may want to check anonymous mech ... } diff --git a/kanidmd/src/lib/idm/macros.rs b/kanidmd/src/lib/idm/macros.rs index 69d723a46..dd6960deb 100644 --- a/kanidmd/src/lib/idm/macros.rs +++ b/kanidmd/src/lib/idm/macros.rs @@ -1,12 +1,19 @@ #[cfg(test)] macro_rules! entry_str_to_account { ($entry_str:expr) => {{ - use crate::entry::{Entry, EntryNew, EntryValid}; + use crate::entry::{Entry, EntryInvalid, EntryNew}; use crate::idm::account::Account; + use crate::value::Value; - let e: Entry = - unsafe { Entry::unsafe_from_entry_str($entry_str).to_valid_new() }; - let e = unsafe { e.to_valid_committed() }; + let mut e: Entry = Entry::unsafe_from_entry_str($entry_str); + // Add spn, because normally this is generated but in tests we can't. + let spn = e + .get_ava_single_str("name") + .map(|s| Value::new_spn_str(s, "example.com")) + .expect("Failed to munge spn from name!"); + e.set_avas("spn", vec![spn]); + + let e = unsafe { e.to_valid_new().to_valid_committed() }; Account::try_from_entry_no_groups(e).expect("Account conversion failure") }}; diff --git a/kanidmd/src/lib/idm/server.rs b/kanidmd/src/lib/idm/server.rs index 70c45f0a0..1e0e61866 100644 --- a/kanidmd/src/lib/idm/server.rs +++ b/kanidmd/src/lib/idm/server.rs @@ -1,5 +1,6 @@ use crate::audit::AuditScope; -use crate::constants::AUTH_SESSION_TIMEOUT; +use crate::constants::UUID_SYSTEM_CONFIG; +use crate::constants::{AUTH_SESSION_TIMEOUT, PW_MIN_LENGTH}; use crate::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; @@ -20,6 +21,7 @@ use concread::cowcell::{CowCell, CowCellWriteTxn}; use std::collections::BTreeMap; use std::time::Duration; use uuid::Uuid; +use zxcvbn; pub struct IdmServer { // There is a good reason to keep this single thread - it @@ -252,14 +254,67 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { return Err(OperationError::SystemProtectedObject); } - // TODO: Is it a security issue to reveal pw policy checks BEFORE permission is + // Question: Is it a security issue to reveal pw policy checks BEFORE permission is // determined over the credential modification? // // I don't think so - because we should only be showing how STRONG the pw is ... - // is the password long enough? + // password strength and badlisting is always global, rather than per-pw-policy. + // pw-policy as check on the account is about requirements for mfa for example. + // - // check a password badlist + // is the password at least 10 char? + if pce.cleartext.len() < PW_MIN_LENGTH { + return Err(OperationError::PasswordTooShort(PW_MIN_LENGTH)); + } + + // does the password pass zxcvbn? + + // Get related inputs, such as account name, email, etc. + let related: Vec<&str> = vec![ + account.name.as_str(), + account.displayname.as_str(), + account.spn.as_str(), + ]; + + let entropy = try_audit!( + au, + zxcvbn::zxcvbn(pce.cleartext.as_str(), related.as_slice()) + .map_err(|_| OperationError::PasswordEmpty) + ); + + // check account pwpolicy (for 3 or 4)? Do we need pw strength beyond this + // or should we be enforcing mfa instead + if entropy.score() < 3 { + // The password is too week as per: + // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html + let feedback: zxcvbn::feedback::Feedback = entropy + .feedback() + .as_ref() + .ok_or(OperationError::InvalidState) + .map(|v| v.clone()) + .map_err(|e| { + audit_log!(au, "zxcvbn returned no feedback when score < 3"); + e + })?; + + audit_log!(au, "pw feedback -> {:?}", feedback); + + // return Err(OperationError::PasswordTooWeak(feedback)) + return Err(OperationError::PasswordTooWeak); + } + + // check a password badlist to eliminate more content + // we check the password as "lower case" to help eliminate possibilities + let lc_password = PartialValue::new_iutf8s(pce.cleartext.as_str()); + let badlist_entry = try_audit!( + au, + self.qs_write.internal_search_uuid(au, &UUID_SYSTEM_CONFIG) + ); + if badlist_entry.attribute_value_pres("badlist_password", &lc_password) { + audit_log!(au, "Password found in badlist, rejecting"); + return Err(OperationError::PasswordBadListed); + } // it returns a modify let modlist = try_audit!( @@ -737,4 +792,37 @@ mod tests { assert!(r1 == tok_r.secret); }) } + + #[test] + fn test_idm_simple_password_reject_weak() { + run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + // len check + let mut idms_prox_write = idms.proxy_write(); + + let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, "password", None); + let e = idms_prox_write.set_account_password(au, &pce); + assert!(e.is_err()); + + // zxcvbn check + let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, "password1234", None); + let e = idms_prox_write.set_account_password(au, &pce); + assert!(e.is_err()); + + // Check the "name" checking works too (I think admin may hit a common pw rule first) + let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, "admin_nta", None); + let e = idms_prox_write.set_account_password(au, &pce); + assert!(e.is_err()); + + // Check that the demo badlist password is rejected. + let pce = PasswordChangeEvent::new_internal( + &UUID_ADMIN, + "demo_badlist_shohfie3aeci2oobur0aru9uushah6EiPi2woh4hohngoighaiRuepieN3ongoo1", + None, + ); + let e = idms_prox_write.set_account_password(au, &pce); + assert!(e.is_err()); + + assert!(idms_prox_write.commit(au).is_ok()); + }) + } } diff --git a/kanidmd/src/lib/plugins/protected.rs b/kanidmd/src/lib/plugins/protected.rs index 11973aadc..b83b10979 100644 --- a/kanidmd/src/lib/plugins/protected.rs +++ b/kanidmd/src/lib/plugins/protected.rs @@ -26,16 +26,21 @@ lazy_static! { m.insert("may"); // Allow modification of some domain info types for local configuration. m.insert("domain_ssid"); + m.insert("badlist_password"); m }; static ref PVCLASS_SYSTEM: PartialValue = PartialValue::new_class("system"); static ref PVCLASS_TOMBSTONE: PartialValue = PartialValue::new_class("tombstone"); static ref PVCLASS_RECYCLED: PartialValue = PartialValue::new_class("recycled"); static ref PVCLASS_DOMAIN_INFO: PartialValue = PartialValue::new_class("domain_info"); + static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info"); + static ref PVCLASS_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config"); static ref VCLASS_SYSTEM: Value = Value::new_class("system"); static ref VCLASS_TOMBSTONE: Value = Value::new_class("tombstone"); static ref VCLASS_RECYCLED: Value = Value::new_class("recycled"); static ref VCLASS_DOMAIN_INFO: Value = Value::new_class("domain_info"); + static ref VCLASS_SYSTEM_INFO: Value = Value::new_class("system_info"); + static ref VCLASS_SYSTEM_CONFIG: Value = Value::new_class("system_config"); } impl Plugin for Protected { @@ -63,6 +68,8 @@ impl Plugin for Protected { Ok(_) => { if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) || cand.attribute_value_pres("class", &PVCLASS_DOMAIN_INFO) + || cand.attribute_value_pres("class", &PVCLASS_SYSTEM_INFO) + || cand.attribute_value_pres("class", &PVCLASS_SYSTEM_CONFIG) || cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE) || cand.attribute_value_pres("class", &PVCLASS_RECYCLED) { @@ -99,6 +106,8 @@ impl Plugin for Protected { if a == "class" && (v == &(VCLASS_SYSTEM.clone()) || v == &(VCLASS_DOMAIN_INFO.clone()) + || v == &(VCLASS_SYSTEM_INFO.clone()) + || v == &(VCLASS_SYSTEM_CONFIG.clone()) || v == &(VCLASS_TOMBSTONE.clone()) || v == &(VCLASS_RECYCLED.clone())) { @@ -183,6 +192,8 @@ impl Plugin for Protected { Ok(_) => { if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) || cand.attribute_value_pres("class", &PVCLASS_DOMAIN_INFO) + || cand.attribute_value_pres("class", &PVCLASS_SYSTEM_INFO) + || cand.attribute_value_pres("class", &PVCLASS_SYSTEM_CONFIG) || cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE) || cand.attribute_value_pres("class", &PVCLASS_RECYCLED) { diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index a3f684bd0..898e24212 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -1631,12 +1631,14 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_ATTR_DOMAIN_UUID, JSON_SCHEMA_ATTR_DOMAIN_SSID, JSON_SCHEMA_ATTR_GIDNUMBER, + JSON_SCHEMA_ATTR_BADLIST_PASSWORD, JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_DOMAIN_INFO, JSON_SCHEMA_CLASS_POSIXACCOUNT, JSON_SCHEMA_CLASS_POSIXGROUP, + JSON_SCHEMA_CLASS_SYSTEM_CONFIG, ]; let mut audit_si = AuditScope::new("start_initialise_schema_idm"); @@ -1660,7 +1662,10 @@ impl<'a> QueryServerWriteTransaction<'a> { let mut audit_an = AuditScope::new("start_system_core_items"); let res = self .internal_assert_or_create_str(&mut audit_an, JSON_SYSTEM_INFO_V1) - .and_then(|_| self.internal_migrate_or_create_str(&mut audit_an, JSON_DOMAIN_INFO_V1)); + .and_then(|_| self.internal_migrate_or_create_str(&mut audit_an, JSON_DOMAIN_INFO_V1)) + .and_then(|_| { + self.internal_migrate_or_create_str(&mut audit_an, JSON_SYSTEM_CONFIG_V1) + }); audit.append_scope(audit_an); audit_log!(audit, "initialise_idm p1 -> result {:?}", res); debug_assert!(res.is_ok()); @@ -1743,6 +1748,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1, JSON_IDM_ACP_ACP_MANAGE_PRIV_V1, JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1, + JSON_IDM_ACP_SYSTEM_CONFIG_PRIV_V1, ]; let res: Result<(), _> = idm_entries