diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 000000000..e985ba419 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,44 @@ +# Getting Started + +WARNING: This document is still in progress, and due to the high rate of change in the cli +tooling, may be OUT OF DATE or otherwise incorrect. If you have questions, please get +in contact! + +Create the service account + + cargo run -- raw create -H https://localhost:8080 -C ../insecure/ca.pem -D admin example.create.account.json + +Give it permissions + + cargo run -- raw modify -H https://localhost:8080 -C ../insecure/ca.pem -D admin '{"Or": [ {"Eq": ["name", "idm_person_account_create_priv"]}, {"Eq": ["name", "idm_service_account_create_priv"]}, {"Eq": ["name", "idm_account_write_priv"]}, {"Eq": ["name", "idm_group_write_priv"]}, {"Eq": ["name", "idm_people_write_priv"]}, {"Eq": ["name", "idm_group_create_priv"]} ]}' example.modify.idm_admin.json + +Show the account details now: + + cargo run -- raw search -H https://localhost:8080 -C ../insecure/ca.pem -D admin '{"Eq": ["name", "idm_admin"]}' + > Entry { attrs: {"class": ["account", "memberof", "object"], "displayname": ["IDM Admin"], "memberof": ["idm_people_read_priv", "idm_people_write_priv", "idm_group_write_priv", "idm_account_read_priv", "idm_account_write_priv", "idm_service_account_create_priv", "idm_person_account_create_priv", "idm_high_privilege"], "name": ["idm_admin"], "uuid": ["bb852c38-8920-4932-a551-678253cae6ff"]} } + +Set the password + + cargo run -- account credential set_password -H https://localhost:8080 -C ../insecure/ca.pem -D admin idm_admin + +Or even better: + + cargo run -- account credential generate_password -H https://localhost:8080 -C ../insecure/ca.pem -D admin idm_admin + +Show it works: + + cargo run -- self whoami -H 'https://localhost:8080' -C ../insecure/ca.pem -D idm_admin + +Now our service account can create and administer accounts and groups: + + cargo run -- raw create -H https://localhost:8080 -C ../insecure/ca.pem -D idm_admin example.create.group.json + +And of course, as the idm_admin, we can't write back to admin: + + cargo run -- account credential generate_password -H https://localhost:8080 -C ../insecure/ca.pem -D idm_admin admin + +Nor can we escalate privs (we are not allow to modify HP groups): + + cargo run -- raw modify -H https://localhost:8080 -C ../insecure/ca.pem -D idm_admin '{"Eq": ["name", "idm_admins"]}' example.modify.idm_admin.json + +So we have a secure way to manage the identities in the directory, without giving full control to any one account! diff --git a/README.md b/README.md index dc707f438..8d8e7db39 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ In a new terminal, you can now build and run the client tools with: cargo run -- whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem cargo run -- whoami -H https://localhost:8080 -D admin -C ../insecure/ca.pem +For more see [getting started] + +[getting started]: https://github.com/Firstyear/kanidm/blob/master/GETTING_STARTED.html + ## Development and Testing There are tests of various components through the various components of the project. When developing diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index aebd2a2ed..c2f1febe2 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -11,9 +11,9 @@ use std::fs::File; use std::io::Read; use kanidm_proto::v1::{ - AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, Filter, - ModifyList, ModifyRequest, OperationResponse, SearchRequest, SearchResponse, - SingleStringRequest, UserAuthToken, WhoamiResponse, + AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, + Entry, Filter, ModifyList, ModifyRequest, OperationResponse, SearchRequest, SearchResponse, + SetAuthCredential, SingleStringRequest, UserAuthToken, WhoamiResponse, }; use serde_json; @@ -24,6 +24,7 @@ pub enum ClientError { Transport(reqwest::Error), AuthenticationFailed, JsonParse, + EmptyResponse, } #[derive(Debug)] @@ -53,6 +54,17 @@ impl KanidmClient { } } + pub fn new_session(&self) -> Self { + let new_client = + Self::build_reqwest(&self.ca).expect("Unexpected reqwest builder failure!"); + + KanidmClient { + client: new_client, + addr: self.addr.clone(), + ca: self.ca.clone(), + } + } + fn build_reqwest(ca: &Option) -> Result { let client_builder = reqwest::Client::builder().cookie_store(true); @@ -97,6 +109,33 @@ impl KanidmClient { Ok(r) } + fn perform_put_request( + &self, + dest: &str, + request: R, + ) -> Result { + let dest = format!("{}{}", self.addr, dest); + + let req_string = serde_json::to_string(&request).unwrap(); + + let mut response = self + .client + .put(dest.as_str()) + .body(req_string) + .send() + .map_err(|e| ClientError::Transport(e))?; + + match response.status() { + reqwest::StatusCode::OK => {} + unexpect => return Err(ClientError::Http(unexpect)), + } + + // TODO: What about errors + let r: T = response.json().unwrap(); + + Ok(r) + } + fn perform_get_request(&self, dest: &str) -> Result { let dest = format!("{}{}", self.addr, dest); let mut response = self @@ -185,14 +224,6 @@ impl KanidmClient { } // search - pub fn search_str(&self, query: &str) -> Result, ClientError> { - let filter: Filter = serde_json::from_str(query).map_err(|e| { - error!("JSON Parse Failure -> {:?}", e); - ClientError::JsonParse - })?; - self.search(filter) - } - pub fn search(&self, filter: Filter) -> Result, ClientError> { let sr = SearchRequest { filter: filter }; let r: Result = self.perform_post_request("/v1/raw/search", sr); @@ -216,6 +247,13 @@ impl KanidmClient { r.map(|_| ()) } + // delete + pub fn delete(&self, filter: Filter) -> Result<(), ClientError> { + let dr = DeleteRequest { filter: filter }; + let r: Result = self.perform_post_request("/v1/raw/delete", dr); + r.map(|_| ()) + } + // === idm actions here == pub fn idm_account_set_password(&self, cleartext: String) -> Result<(), ClientError> { let s = SingleStringRequest { value: cleartext }; @@ -256,6 +294,36 @@ impl KanidmClient { self.perform_get_request(format!("/v1/account/{}", id).as_str()) } + // different ways to set the primary credential? + // not sure how to best expose this. + pub fn idm_account_primary_credential_set_password( + &self, + id: &str, + pw: &str, + ) -> Result<(), ClientError> { + let r = SetAuthCredential::Password(pw.to_string()); + let res: Result, _> = self.perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ); + res.map(|_| ()) + } + + pub fn idm_account_primary_credential_set_generated( + &self, + id: &str, + ) -> Result { + let r = SetAuthCredential::GeneratePassword; + self.perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .and_then(|v| match v { + Some(p) => Ok(p), + None => Err(ClientError::EmptyResponse), + }) + } + // ==== schema pub fn idm_schema_list(&self) -> Result, ClientError> { self.perform_get_request("/v1/schema") diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index 875ca39c8..acdff7a09 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -186,7 +186,7 @@ fn test_server_search() { assert!(res.is_ok()); let rset = rsclient - .search_str("{\"Eq\":[\"name\", \"admin\"]}") + .search(Filter::Eq("name".to_string(), "admin".to_string())) .unwrap(); println!("{:?}", rset); let e = rset.first().unwrap(); @@ -224,6 +224,57 @@ fn test_server_admin_change_simple_password() { }); } +// Add a test for reseting another accounts pws via the rest api +#[test] +fn test_server_admin_reset_simple_password() { + run_test(|rsclient: KanidmClient| { + let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD); + assert!(res.is_ok()); + // Create a diff account + let e: Entry = serde_json::from_str( + r#"{ + "attrs": { + "class": ["person", "account"], + "name": ["testperson"], + "displayname": ["testperson"] + } + }"#, + ) + .unwrap(); + + // Not logged in - should fail! + let res = rsclient.create(vec![e.clone()]); + assert!(res.is_ok()); + // By default, admin's can't actually administer accounts, so mod them into + // the account admin group. + let f = Filter::Eq("name".to_string(), "idm_account_write_priv".to_string()); + let m = ModifyList::new_list(vec![Modify::Present( + "member".to_string(), + "idm_admins".to_string(), + )]); + let res = rsclient.modify(f.clone(), m.clone()); + assert!(res.is_ok()); + + // Now set it's password. + let res = rsclient.idm_account_primary_credential_set_password("testperson", "password"); + assert!(res.is_ok()); + // Check it stuck. + let tclient = rsclient.new_session(); + assert!(tclient + .auth_simple_password("testperson", "password") + .is_ok()); + + // Generate a pw instead + let res = rsclient.idm_account_primary_credential_set_generated("testperson"); + assert!(res.is_ok()); + let gpw = res.unwrap(); + let tclient = rsclient.new_session(); + assert!(tclient + .auth_simple_password("testperson", gpw.as_str()) + .is_ok()); + }); +} + // test the rest group endpoint. #[test] fn test_server_rest_group_read() { diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index 729ec1b2d..a232325f5 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -308,6 +308,15 @@ pub struct AuthResponse { pub state: AuthState, } +// Types needed for setting credentials +#[derive(Debug, Serialize, Deserialize)] +pub enum SetAuthCredential { + Password(String), + GeneratePassword, + // TOTP() + // Webauthn(response) +} + /* Recycle Requests area */ // Only two actions on recycled is possible. Search and Revive. diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index f949f9323..91fb6e324 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -10,8 +10,11 @@ path = "src/main.rs" [dependencies] kanidm_client = { path = "../kanidm_client" } +kanidm_proto = { path = "../kanidm_proto" } rpassword = "0.4" structopt = { version = "0.2", default-features = false } log = "0.4" env_logger = "0.6" +serde = "1.0" +serde_json = "1.0" diff --git a/kanidm_tools/example.create.account.json b/kanidm_tools/example.create.account.json new file mode 100644 index 000000000..b6d3eed8a --- /dev/null +++ b/kanidm_tools/example.create.account.json @@ -0,0 +1,10 @@ +[ + { + "name": ["idm_admin"], + "class": ["account"], + "displayname": ["IDM Admin"], + "description": ["Default IDM Admin"] + } +] + + diff --git a/kanidm_tools/example.create.group.json b/kanidm_tools/example.create.group.json new file mode 100644 index 000000000..3487fd5cb --- /dev/null +++ b/kanidm_tools/example.create.group.json @@ -0,0 +1,6 @@ +[ + { + "name": ["demo_group"], + "class": ["group"] + } +] diff --git a/kanidm_tools/example.modify.idm_admin.json b/kanidm_tools/example.modify.idm_admin.json new file mode 100644 index 000000000..5b34fd1b8 --- /dev/null +++ b/kanidm_tools/example.modify.idm_admin.json @@ -0,0 +1,5 @@ +[ + {"Present": ["member", "idm_admin"]} +] + + diff --git a/kanidm_tools/example.modify.json b/kanidm_tools/example.modify.json new file mode 100644 index 000000000..eddbc1e1e --- /dev/null +++ b/kanidm_tools/example.modify.json @@ -0,0 +1,4 @@ +[ + { "Purged": "name" }, + { "Present": ["name", "demo_group"] } +] diff --git a/kanidm_tools/src/main.rs b/kanidm_tools/src/main.rs index f10f08775..58ee8928a 100644 --- a/kanidm_tools/src/main.rs +++ b/kanidm_tools/src/main.rs @@ -1,7 +1,16 @@ extern crate structopt; use kanidm_client::KanidmClient; +use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList}; +use serde::de::DeserializeOwned; use std::path::PathBuf; use structopt::StructOpt; + +use std::collections::BTreeMap; +use std::error::Error; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + extern crate env_logger; #[macro_use] extern crate log; @@ -40,25 +49,87 @@ impl CommonOpt { } #[derive(Debug, StructOpt)] -struct SearchOpt { +struct FilterOpt { #[structopt()] filter: String, #[structopt(flatten)] commonopts: CommonOpt, } +#[derive(Debug, StructOpt)] +struct CreateOpt { + #[structopt(parse(from_os_str))] + file: Option, + #[structopt(flatten)] + commonopts: CommonOpt, +} + +#[derive(Debug, StructOpt)] +struct ModifyOpt { + #[structopt(flatten)] + commonopts: CommonOpt, + #[structopt()] + filter: String, + #[structopt(parse(from_os_str))] + file: Option, +} + +#[derive(Debug, StructOpt)] +enum RawOpt { + #[structopt(name = "search")] + Search(FilterOpt), + #[structopt(name = "create")] + Create(CreateOpt), + #[structopt(name = "modify")] + Modify(ModifyOpt), + #[structopt(name = "delete")] + Delete(FilterOpt), +} + +#[derive(Debug, StructOpt)] +struct AccountCommonOpt { + #[structopt()] + account_id: String, +} + +#[derive(Debug, StructOpt)] +struct AccountCredentialSet { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt()] + application_id: Option, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +enum AccountCredential { + #[structopt(name = "set_password")] + SetPassword(AccountCredentialSet), + #[structopt(name = "generate_password")] + GeneratePassword(AccountCredentialSet), +} + #[derive(Debug, StructOpt)] enum AccountOpt { + #[structopt(name = "credential")] + Credential(AccountCredential), +} + +#[derive(Debug, StructOpt)] +enum SelfOpt { + #[structopt(name = "whoami")] + Whoami(CommonOpt), #[structopt(name = "set_password")] SetPassword(CommonOpt), } #[derive(Debug, StructOpt)] enum ClientOpt { - #[structopt(name = "search")] - Search(SearchOpt), - #[structopt(name = "whoami")] - Whoami(CommonOpt), + #[structopt(name = "raw")] + Raw(RawOpt), + #[structopt(name = "self")] + CSelf(SelfOpt), #[structopt(name = "account")] Account(AccountOpt), } @@ -66,15 +137,31 @@ enum ClientOpt { impl ClientOpt { fn debug(&self) -> bool { match self { - ClientOpt::Whoami(copt) => copt.debug, - ClientOpt::Search(sopt) => sopt.commonopts.debug, + ClientOpt::Raw(ropt) => match ropt { + RawOpt::Search(sopt) => sopt.commonopts.debug, + RawOpt::Create(copt) => copt.commonopts.debug, + RawOpt::Modify(mopt) => mopt.commonopts.debug, + RawOpt::Delete(dopt) => dopt.commonopts.debug, + }, + ClientOpt::CSelf(csopt) => match csopt { + SelfOpt::Whoami(copt) => copt.debug, + SelfOpt::SetPassword(copt) => copt.debug, + }, ClientOpt::Account(aopt) => match aopt { - AccountOpt::SetPassword(copt) => copt.debug, + _ => false, }, } } } +fn read_file>(path: P) -> Result> { + let f = File::open(path)?; + let r = BufReader::new(f); + + let t: T = serde_json::from_reader(r)?; + Ok(t) +} + fn main() { let opt = ClientOpt::from_args(); @@ -86,31 +173,69 @@ fn main() { env_logger::init(); match opt { - ClientOpt::Whoami(copt) => { - let client = copt.to_client(); + ClientOpt::Raw(ropt) => match ropt { + RawOpt::Search(sopt) => { + let client = sopt.commonopts.to_client(); - match client.whoami() { - Ok(o_ent) => match o_ent { - Some((ent, uat)) => { - debug!("{:?}", ent); - println!("{}", uat); + let filter: Filter = serde_json::from_str(sopt.filter.as_str()).unwrap(); + let rset = client.search(filter).unwrap(); + + rset.iter().for_each(|e| { + println!("{:?}", e); + }); + } + RawOpt::Create(copt) => { + let client = copt.commonopts.to_client(); + // Read the file? + match copt.file { + Some(p) => { + let r_entries: Vec>> = read_file(p).unwrap(); + let entries = r_entries.into_iter().map(|b| Entry { attrs: b }).collect(); + client.create(entries).unwrap() } - None => println!("Unauthenticated"), - }, - Err(e) => println!("Error: {:?}", e), + None => { + println!("Must provide a file"); + } + } } - } - ClientOpt::Search(sopt) => { - let client = sopt.commonopts.to_client(); - - let rset = client.search_str(sopt.filter.as_str()).unwrap(); - - for e in rset { - println!("{:?}", e); + RawOpt::Modify(mopt) => { + let client = mopt.commonopts.to_client(); + // Read the file? + match mopt.file { + Some(p) => { + let filter: Filter = serde_json::from_str(mopt.filter.as_str()).unwrap(); + let r_list: Vec = read_file(p).unwrap(); + let modlist = ModifyList::new_list(r_list); + client.modify(filter, modlist).unwrap() + } + None => { + println!("Must provide a file"); + } + } } - } - ClientOpt::Account(aopt) => match aopt { - AccountOpt::SetPassword(copt) => { + RawOpt::Delete(dopt) => { + let client = dopt.commonopts.to_client(); + let filter: Filter = serde_json::from_str(dopt.filter.as_str()).unwrap(); + client.delete(filter).unwrap(); + } + }, + ClientOpt::CSelf(csopt) => match csopt { + SelfOpt::Whoami(copt) => { + let client = copt.to_client(); + + match client.whoami() { + Ok(o_ent) => match o_ent { + Some((ent, uat)) => { + debug!("{:?}", ent); + println!("{}", uat); + } + None => println!("Unauthenticated"), + }, + Err(e) => println!("Error: {:?}", e), + } + } + + SelfOpt::SetPassword(copt) => { let client = copt.to_client(); let password = rpassword::prompt_password_stderr("Enter new password: ").unwrap(); @@ -118,5 +243,37 @@ fn main() { client.idm_account_set_password(password).unwrap(); } }, + ClientOpt::Account(aopt) => match aopt { + // id/cred/primary/set + AccountOpt::Credential(acopt) => match acopt { + AccountCredential::SetPassword(acsopt) => { + let client = acsopt.copt.to_client(); + let password = rpassword::prompt_password_stderr( + format!("Enter new password for {}: ", acsopt.aopts.account_id).as_str(), + ) + .unwrap(); + + client + .idm_account_primary_credential_set_password( + acsopt.aopts.account_id.as_str(), + password.as_str(), + ) + .unwrap(); + } + AccountCredential::GeneratePassword(acsopt) => { + let client = acsopt.copt.to_client(); + + let npw = client + .idm_account_primary_credential_set_generated( + acsopt.aopts.account_id.as_str(), + ) + .unwrap(); + println!( + "Generated password for {}: {}", + acsopt.aopts.account_id, npw + ); + } + }, // end AccountOpt::Credential + }, } } diff --git a/kanidmd/src/lib/access.rs b/kanidmd/src/lib/access.rs index e8e6c2f5d..fb329109f 100644 --- a/kanidmd/src/lib/access.rs +++ b/kanidmd/src/lib/access.rs @@ -555,7 +555,9 @@ pub trait AccessControlsTransaction { }) .collect(); - audit_log!(audit, "Related acs -> {:?}", related_acp); + related_acp.iter().for_each(|racp| { + audit_log!(audit, "Related acs -> {:?}", racp.acp.name); + }); // Get the set of attributes requested by the caller // TODO #69: This currently @@ -684,7 +686,9 @@ pub trait AccessControlsTransaction { }) .collect(); - audit_log!(audit, "Related acs -> {:?}", related_acp); + related_acp.iter().for_each(|racp| { + audit_log!(audit, "Related acs -> {:?}", racp.acp.name); + }); // build two sets of "requested pres" and "requested rem" let requested_pres: BTreeSet<&str> = me @@ -1020,7 +1024,9 @@ pub trait AccessControlsTransaction { }) .collect(); - audit_log!(audit, "Related acs -> {:?}", related_acp); + related_acp.iter().for_each(|racp| { + audit_log!(audit, "Related acs -> {:?}", racp.acp.name); + }); // For each entry let r = entries.iter().fold(true, |acc, e| { diff --git a/kanidmd/src/lib/actors/v1.rs b/kanidmd/src/lib/actors/v1.rs index e347624cc..3910858c9 100644 --- a/kanidmd/src/lib/actors/v1.rs +++ b/kanidmd/src/lib/actors/v1.rs @@ -7,7 +7,7 @@ use crate::event::{ AuthEvent, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, SearchEvent, SearchResult, WhoamiResult, }; -use crate::idm::event::PasswordChangeEvent; +use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent}; use kanidm_proto::v1::OperationError; use crate::filter::{Filter, FilterInvalid}; @@ -17,7 +17,8 @@ use crate::server::{QueryServer, QueryServerTransaction}; use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::{ AuthRequest, AuthResponse, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, - SearchRequest, SearchResponse, SingleStringRequest, UserAuthToken, WhoamiResponse, + SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken, + WhoamiResponse, }; use actix::prelude::*; @@ -160,6 +161,33 @@ impl Message for IdmAccountSetPasswordMessage { type Result = Result; } +pub struct InternalCredentialSetMessage { + pub uat: Option, + pub uuid_or_name: String, + pub appid: Option, + pub sac: SetAuthCredential, +} + +impl InternalCredentialSetMessage { + pub fn new( + uat: Option, + uuid_or_name: String, + appid: Option, + sac: SetAuthCredential, + ) -> Self { + InternalCredentialSetMessage { + uat: uat, + uuid_or_name: uuid_or_name, + appid: appid, + sac: sac, + } + } +} + +impl Message for InternalCredentialSetMessage { + type Result = Result, OperationError>; +} + // =========================================================== pub struct QueryServerV1 { @@ -480,6 +508,81 @@ impl Handler for QueryServerV1 { } } +impl Handler for QueryServerV1 { + type Result = Result, OperationError>; + + fn handle(&mut self, msg: InternalCredentialSetMessage, _: &mut Self::Context) -> Self::Result { + let mut audit = AuditScope::new("internal_credential_set_message"); + let res = audit_segment!(&mut audit, || { + let mut idms_prox_write = self.idms.proxy_write(); + + // given the uuid_or_name, determine the target uuid. + // We can either do this by trying to parse the name or by creating a filter + // to find the entry - there are risks to both TBH ... especially when the uuid + // is also an entries name, but that they aren't the same entry. + let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) { + Ok(u) => u, + Err(_) => idms_prox_write + .qs_write + .name_to_uuid(&mut audit, msg.uuid_or_name.as_str()) + .map_err(|e| { + audit_log!(&mut audit, "Error resolving id to target"); + e + })?, + }; + + // What type of auth set did we recieve? + match msg.sac { + SetAuthCredential::Password(cleartext) => { + let pce = PasswordChangeEvent::from_parts( + &mut audit, + &idms_prox_write.qs_write, + msg.uat, + target_uuid, + cleartext, + msg.appid, + ) + .map_err(|e| { + audit_log!( + audit, + "Failed to begin internal_credential_set_message: {:?}", + e + ); + e + })?; + idms_prox_write + .set_account_password(&mut audit, &pce) + .and_then(|_| idms_prox_write.commit(&mut audit)) + .map(|_| None) + } + SetAuthCredential::GeneratePassword => { + let gpe = GeneratePasswordEvent::from_parts( + &mut audit, + &idms_prox_write.qs_write, + msg.uat, + target_uuid, + msg.appid, + ) + .map_err(|e| { + audit_log!( + audit, + "Failed to begin internal_credential_set_message: {:?}", + e + ); + e + })?; + idms_prox_write + .generate_account_password(&mut audit, &gpe) + .and_then(|r| idms_prox_write.commit(&mut audit).map(|_| r)) + .map(|v| Some(v)) + } + } + }); + self.log.do_send(audit); + res + } +} + // These below are internal only types. impl Handler for QueryServerV1 { diff --git a/kanidmd/src/lib/constants.rs b/kanidmd/src/lib/constants.rs index b67365038..7cb00f8d8 100644 --- a/kanidmd/src/lib/constants.rs +++ b/kanidmd/src/lib/constants.rs @@ -201,6 +201,18 @@ pub static JSON_IDM_PERSON_ACCOUNT_CREATE_PRIV_V1: &'static str = r#"{ } }"#; +// IDM_GROUP_CREATE_PRIV +pub static _UUID_IDM_GROUP_CREATE_PRIV: &'static str = "00000000-0000-0000-0000-000000000015"; +pub static JSON_IDM_GROUP_CREATE_PRIV_V1: &'static str = r#"{ + "attrs": { + "class": ["group", "object"], + "name": ["idm_group_create_priv"], + "uuid": ["00000000-0000-0000-0000-000000000015"], + "description": ["Builtin IDM Group for granting elevated group creation permissions."], + "member": ["00000000-0000-0000-0000-000000000001"] + } +}"#; + // This must be the last group to init to include the UUID of the other high priv groups. pub static _UUID_IDM_HIGH_PRIVILEGE: &'static str = "00000000-0000-0000-0000-000000001000"; pub static JSON_IDM_HIGH_PRIVILEGE_V1: &'static str = r#"{ @@ -590,6 +602,7 @@ pub static JSON_IDM_ACP_SERVICE_ACCOUNT_CREATE_V1: &'static str = r#"{ "class", "name", "displayname", + "description", "primary_credential", "ssh_publickey" ], @@ -967,6 +980,37 @@ pub static JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1: &'static str = r#"{ // 21 - anonymous / everyone schema read. +// 22 - group create right +pub static _UUID_IDM_ACP_GROUP_CREATE_V1: &'static str = "00000000-0000-0000-0000-ffffff000022"; +pub static JSON_IDM_ACP_GROUP_CREATE_V1: &'static str = r#"{ + "attrs": { + "class": [ + "object", + "access_control_profile", + "access_control_create" + ], + "name": ["idm_acp_group_create"], + "uuid": ["00000000-0000-0000-0000-ffffff000022"], + "description": ["Builtin IDM Control for creating groups in the directory"], + "acp_enable": ["true"], + "acp_receiver": [ + "{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000015\"]}" + ], + "acp_targetscope": [ + "{\"And\": [{\"Eq\": [\"class\",\"group\"]}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}" + ], + "acp_create_attr": [ + "class", + "name", + "description", + "member" + ], + "acp_create_class": [ + "object", "group" + ] + } +}"#; + // Anonymous should be the last opbject in the range here. pub static JSON_ANONYMOUS_V1: &'static str = r#"{ "attrs": { diff --git a/kanidmd/src/lib/core.rs b/kanidmd/src/lib/core.rs index 526b022bd..2072d5bdf 100644 --- a/kanidmd/src/lib/core.rs +++ b/kanidmd/src/lib/core.rs @@ -15,8 +15,9 @@ use crate::config::Configuration; // SearchResult use crate::actors::v1::QueryServerV1; use crate::actors::v1::{ - AuthMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalSearchMessage, - ModifyMessage, SearchMessage, WhoamiMessage, + AuthMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, + InternalCredentialSetMessage, InternalSearchMessage, ModifyMessage, SearchMessage, + WhoamiMessage, }; use crate::async_log; use crate::audit::AuditScope; @@ -34,7 +35,7 @@ use crate::value::PartialValue; use kanidm_proto::v1::OperationError; use kanidm_proto::v1::{ AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest, - SingleStringRequest, UserAuthToken, + SetAuthCredential, SingleStringRequest, UserAuthToken, }; use uuid::Uuid; @@ -108,9 +109,9 @@ macro_rules! json_event_post { "Json Decode Failed: {:?}", e )))), - } - }, - ) + } // end match + }, // end closure + ) // end and_then }}; } @@ -218,6 +219,69 @@ fn json_rest_event_get_id( Box::new(res) } +fn json_rest_event_credential_put( + id: String, + cred_id: Option, + req: HttpRequest, + state: State, +) -> impl Future { + // what do we need here? + // * a filter of the id to match + class + // * the id of the credential + // * The SetAuthCredential + // * turn into a modlist + + // Copy the max size since we move it. + let max_size = state.max_size; + let uat = get_current_user(&req); + + req.payload() + .from_err() + .fold(BytesMut::new(), move |mut body, chunk| { + // limit max size of in-memory payload + if (body.len() + chunk.len()) > max_size { + Err(error::ErrorBadRequest("overflow")) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + // `Future::and_then` can be used to merge an asynchronous workflow with a + // synchronous workflow + .and_then( + move |body| -> Box> { + let r_obj = serde_json::from_slice::(&body); + + match r_obj { + Ok(obj) => { + let m_obj = InternalCredentialSetMessage::new(uat, id, cred_id, obj); + let res = state.qe.send(m_obj).from_err().and_then(|res| match res { + Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), + Err(e) => Ok(HttpResponse::InternalServerError().json(e)), + }); + + Box::new(res) + } + Err(e) => Box::new(future::err(error::ErrorBadRequest(format!( + "Json Decode Failed: {:?}", + e + )))), + } // end match + }, + ) // end and_then +} + +// Okay, so a put normally needs +// * filter of what we are working on (id + class) +// * a BTreeMap> that we turn into a modlist. +// +// OR +// * filter of what we are working on (id + class) +// * a Vec that we are changing +// * the attr name (as a param to this in path) +// +// json_rest_event_put_id(path, req, state + fn schema_get( (req, state): (HttpRequest, State), ) -> impl Future { @@ -314,6 +378,13 @@ fn account_get_id( json_rest_event_get_id(path, req, state, filter) } +fn account_put_id_credential_primary( + (path, req, state): (Path, HttpRequest, State), +) -> impl Future { + let id = path.into_inner(); + json_rest_event_credential_put(id, None, req, state) +} + fn group_get( (req, state): (HttpRequest, State), ) -> impl Future { @@ -928,6 +999,12 @@ pub fn create_server_core(config: Configuration) { r.method(http::Method::GET).with(do_nothing) // add delete }) + .resource("/v1/account/{id}/_credential/primary", |r| { + // Set a new primary credential value. + // in future this will tie in to claims. + r.method(http::Method::PUT) + .with_async(account_put_id_credential_primary) + }) .resource("/v1/account/{id}/_credential/{cid}/_lock", |r| { r.method(http::Method::GET).with(do_nothing) // add post, delete diff --git a/kanidmd/src/lib/idm/event.rs b/kanidmd/src/lib/idm/event.rs index 97f835118..cc3e12a33 100644 --- a/kanidmd/src/lib/idm/event.rs +++ b/kanidmd/src/lib/idm/event.rs @@ -5,7 +5,7 @@ use crate::server::QueryServerWriteTransaction; use uuid::Uuid; -use kanidm_proto::v1::OperationError; +use kanidm_proto::v1::{OperationError, UserAuthToken}; #[derive(Debug)] pub struct PasswordChangeEvent { @@ -40,4 +40,47 @@ impl PasswordChangeEvent { appid: None, }) } + + pub fn from_parts( + audit: &mut AuditScope, + qs: &QueryServerWriteTransaction, + uat: Option, + target: Uuid, + cleartext: String, + appid: Option, + ) -> Result { + let e = Event::from_rw_uat(audit, qs, uat)?; + + Ok(PasswordChangeEvent { + event: e, + target: target, + cleartext: cleartext, + appid: appid, + }) + } +} + +#[derive(Debug)] +pub struct GeneratePasswordEvent { + pub event: Event, + pub target: Uuid, + pub appid: Option, +} + +impl GeneratePasswordEvent { + pub fn from_parts( + audit: &mut AuditScope, + qs: &QueryServerWriteTransaction, + uat: Option, + target: Uuid, + appid: Option, + ) -> Result { + let e = Event::from_rw_uat(audit, qs, uat)?; + + Ok(GeneratePasswordEvent { + event: e, + target: target, + appid: appid, + }) + } } diff --git a/kanidmd/src/lib/idm/server.rs b/kanidmd/src/lib/idm/server.rs index 5f95aacb9..80249b1d8 100644 --- a/kanidmd/src/lib/idm/server.rs +++ b/kanidmd/src/lib/idm/server.rs @@ -3,9 +3,9 @@ use crate::constants::AUTH_SESSION_TIMEOUT; use crate::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; -use crate::idm::event::PasswordChangeEvent; +use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent}; use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction}; -use crate::utils::{uuid_from_duration, SID}; +use crate::utils::{password_from_random, uuid_from_duration, SID}; use crate::value::PartialValue; use kanidm_proto::v1::AuthState; @@ -282,6 +282,49 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { self.set_account_password(au, &pce) } + pub fn generate_account_password( + &mut self, + au: &mut AuditScope, + gpe: &GeneratePasswordEvent, + ) -> Result { + // Get the account + let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &gpe.target)); + let account = try_audit!(au, Account::try_from_entry(account_entry)); + // Ask if tis all good - this step checks pwpolicy and such + + // Deny the change if the target account is anonymous! + if account.is_anonymous() { + return Err(OperationError::SystemProtectedObject); + } + + // Generate a new random, long pw. + // Because this is generated, we can bypass policy checks! + let cleartext = password_from_random(); + + // check a password badlist - even if generated, we still don't want to + // reuse something that has been disclosed. + + // it returns a modify + let modlist = try_audit!(au, account.gen_password_mod(cleartext.as_str(), &gpe.appid)); + audit_log!(au, "processing change {:?}", modlist); + // given the new credential generate a modify + // We use impersonate here to get the event from ae + try_audit!( + au, + self.qs_write.impersonate_modify( + au, + // Filter as executed + filter!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))), + // Filter as intended (acp) + filter_all!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))), + modlist, + &gpe.event, + ) + ); + + Ok(cleartext) + } + pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> { self.qs_write.commit(au) } diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index a0c533b13..7c186d475 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -1583,6 +1583,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_IDM_PEOPLE_WRITE_PRIV_V1, JSON_IDM_PEOPLE_READ_PRIV_V1, JSON_IDM_GROUP_WRITE_PRIV_V1, + JSON_IDM_GROUP_CREATE_PRIV_V1, JSON_IDM_ACCOUNT_WRITE_PRIV_V1, JSON_IDM_ACCOUNT_READ_PRIV_V1, JSON_IDM_RADIUS_SERVERS_V1, @@ -1616,6 +1617,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_IDM_ACP_SCHEMA_WRITE_ATTRS_PRIV_V1, JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1, JSON_IDM_ACP_ACP_MANAGER_PRIV_V1, + JSON_IDM_ACP_GROUP_CREATE_V1, ]; let res: Result<(), _> = idm_entries diff --git a/kanidmd/src/lib/utils.rs b/kanidmd/src/lib/utils.rs index efe28f345..1c5a0a685 100644 --- a/kanidmd/src/lib/utils.rs +++ b/kanidmd/src/lib/utils.rs @@ -1,6 +1,9 @@ use std::time::Duration; use uuid::{Builder, Uuid}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + pub type SID = [u8; 4]; fn uuid_from_u64_u32(a: u64, b: u32, sid: &SID) -> Uuid { @@ -17,6 +20,11 @@ pub fn uuid_from_duration(d: Duration, sid: &SID) -> Uuid { uuid_from_u64_u32(d.as_secs(), d.subsec_nanos(), sid) } +pub fn password_from_random() -> String { + let rand_string: String = thread_rng().sample_iter(&Alphanumeric).take(48).collect(); + rand_string +} + #[cfg(test)] mod tests { use crate::utils::uuid_from_duration;