diff --git a/rsidm_client/Cargo.toml b/rsidm_client/Cargo.toml index a1c0355b6..c1d81930c 100644 --- a/rsidm_client/Cargo.toml +++ b/rsidm_client/Cargo.toml @@ -9,10 +9,10 @@ log = "0.4" env_logger = "0.6" reqwest = "0.9" rsidm_proto = { path = "../rsidm_proto" } +serde_json = "1.0" [dev-dependencies] tokio = "0.1" actix = "0.7" rsidm = { path = "../rsidmd" } futures = "0.1" -serde_json = "1.0" diff --git a/rsidm_client/src/lib.rs b/rsidm_client/src/lib.rs index 83c26ae9a..58e518b22 100644 --- a/rsidm_client/src/lib.rs +++ b/rsidm_client/src/lib.rs @@ -1,8 +1,27 @@ -// #![deny(warnings)] +#![deny(warnings)] #![warn(unused_extern_crates)] +#[macro_use] +extern crate log; + +use serde_json; + use reqwest; +use rsidm_proto::v1::{ + AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, + OperationResponse, UserAuthToken, WhoamiResponse, +}; + +#[derive(Debug)] +pub enum ClientError { + Unauthorized, + Http(reqwest::StatusCode), + Transport(reqwest::Error), + AuthenticationFailed, +} + +#[derive(Debug)] pub struct RsidmClient { client: reqwest::Client, addr: String, @@ -20,10 +39,156 @@ impl RsidmClient { } } + fn auth_step_init(&self, ident: &str, appid: Option<&str>) -> Result { + // TODO: Way to avoid formatting so much? + let auth_dest = format!("{}/v1/auth", self.addr); + + let auth_init = AuthRequest { + step: AuthStep::Init(ident.to_string(), appid.map(|s| s.to_string())), + }; + + // Handle this! + let mut response = self + .client + .post(auth_dest.as_str()) + .body(serde_json::to_string(&auth_init).expect("Generated invalid initstep?!")) + .send() + .map_err(|e| ClientError::Transport(e))?; + + match response.status() { + reqwest::StatusCode::OK => {} + unexpect => return Err(ClientError::Http(unexpect)), + } + // Check that we got the next step + let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + + Ok(r.state) + } + // auth + pub fn auth_anonymous(&self) -> Result { + let _state = match self.auth_step_init("anonymous", None) { + Ok(s) => s, + Err(e) => return Err(e), + }; + + // TODO: Avoid creating this so much? + let auth_dest = format!("{}/v1/auth", self.addr); + + // Check state for auth continue contains anonymous. + + let auth_anon = AuthRequest { + step: AuthStep::Creds(vec![AuthCredential::Anonymous]), + }; + + let mut response = self + .client + .post(auth_dest.as_str()) + .body(serde_json::to_string(&auth_anon).unwrap()) + .send() + .map_err(|e| ClientError::Transport(e))?; + + match response.status() { + reqwest::StatusCode::OK => {} + unexpect => return Err(ClientError::Http(unexpect)), + } + // Check that we got the next step + let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + + match r.state { + AuthState::Success(uat) => { + debug!("==> Authed as uat; {:?}", uat); + Ok(uat) + } + _ => Err(ClientError::AuthenticationFailed), + } + } + + pub fn auth_simple_password( + &self, + ident: &str, + password: &str, + ) -> Result { + // TODO: Way to avoid formatting so much? + let auth_dest = format!("{}/v1/auth", self.addr); + + let _state = match self.auth_step_init(ident, None) { + Ok(s) => s, + Err(e) => return Err(e), + }; + + // Send the credentials required now + let auth_req = AuthRequest { + step: AuthStep::Creds(vec![AuthCredential::Password(password.to_string())]), + }; + + let mut response = self + .client + .post(auth_dest.as_str()) + .body(serde_json::to_string(&auth_req).unwrap()) + .send() + .map_err(|e| ClientError::Transport(e))?; + + match response.status() { + reqwest::StatusCode::OK => {} + unexpect => return Err(ClientError::Http(unexpect)), + } + + let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + + match r.state { + AuthState::Success(uat) => { + debug!("==> Authed as uat; {:?}", uat); + Ok(uat) + } + _ => Err(ClientError::AuthenticationFailed), + } + } + // whoami + pub fn whoami(&self) -> Result, ClientError> { + let whoami_dest = format!("{}/v1/whoami", self.addr); + let mut response = self.client.get(whoami_dest.as_str()).send().unwrap(); + // https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html + + match response.status() { + // Continue to process. + reqwest::StatusCode::OK => {} + reqwest::StatusCode::UNAUTHORIZED => return Ok(None), + unexpect => return Err(ClientError::Http(unexpect)), + } + + let r: WhoamiResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + + Ok(Some((r.youare, r.uat))) + } + // search // create + pub fn create(&self, entries: Vec) -> Result<(), ClientError> { + let c = CreateRequest { entries: entries }; + + // TODO: Avoid formatting this so much! + let dest = format!("{}/v1/create", self.addr); + + let mut response = self + .client + .post(dest.as_str()) + .body(serde_json::to_string(&c).unwrap()) + .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: OperationResponse = + serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + Ok(()) + } + // modify // } diff --git a/rsidm_client/tests/proto_v1_test.rs b/rsidm_client/tests/proto_v1_test.rs index ddb8d8f58..f94fd528a 100644 --- a/rsidm_client/tests/proto_v1_test.rs +++ b/rsidm_client/tests/proto_v1_test.rs @@ -7,16 +7,15 @@ extern crate actix; use actix::prelude::*; extern crate rsidm; +extern crate rsidm_client; extern crate rsidm_proto; extern crate serde_json; +use rsidm_client::RsidmClient; + use rsidm::config::{Configuration, IntegrationTestConfig}; -use rsidm::constants::UUID_ADMIN; use rsidm::core::create_server_core; -use rsidm_proto::v1::{ - AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, - OperationResponse, -}; +use rsidm_proto::v1::Entry; extern crate reqwest; @@ -36,7 +35,8 @@ static ADMIN_TEST_PASSWORD: &'static str = "integration test admin password"; // Test external behaviorus of the service. -fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) { +fn run_test(test_fn: fn(RsidmClient) -> ()) { + // ::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug"); let _ = env_logger::builder().is_test(true).try_init(); let (tx, rx) = mpsc::channel(); let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst); @@ -70,13 +70,10 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) { // later we could accept fixture as it's own future for re-use // Setup the client, and the address we selected. - let client = reqwest::Client::builder() - .cookie_store(true) - .build() - .expect("Unexpected reqwest builder failure!"); let addr = format!("http://127.0.0.1:{}", port); + let rsclient = RsidmClient::new(addr.as_str()); - test_fn(client, addr.as_str()); + test_fn(rsclient); // We DO NOT need teardown, as sqlite is in mem // let the tables hit the floor @@ -84,8 +81,8 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) { } #[test] -fn test_server_proto() { - run_test(|client: reqwest::Client, addr: &str| { +fn test_server_create() { + run_test(|rsclient: RsidmClient| { let e: Entry = serde_json::from_str( r#"{ "attrs": { @@ -98,168 +95,54 @@ fn test_server_proto() { ) .unwrap(); - let c = CreateRequest { - entries: vec![e], - user_uuid: UUID_ADMIN.to_string(), - }; + // Not logged in - should fail! + let res = rsclient.create(vec![e.clone()]); + assert!(res.is_err()); - let dest = format!("{}/v1/create", addr); + let a_res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD); + assert!(a_res.is_ok()); - let mut response = client - .post(dest.as_str()) - .body(serde_json::to_string(&c).unwrap()) - .send() - .unwrap(); - - println!("{:?}", response); - let r: OperationResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); - - println!("{:?}", r); - - // deserialise the response here - // check it's valid. - - () + let res = rsclient.create(vec![e]); + assert!(res.is_ok()); }); } #[test] fn test_server_whoami_anonymous() { - run_test(|client: reqwest::Client, addr: &str| { + run_test(|rsclient: RsidmClient| { // First show we are un-authenticated. - let whoami_dest = format!("{}/v1/whoami", addr); - let auth_dest = format!("{}/v1/auth", addr); - - let response = client.get(whoami_dest.as_str()).send().unwrap(); - - // https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html - println!("{:?}", response); - - assert!(response.status() == reqwest::StatusCode::UNAUTHORIZED); + let pre_res = rsclient.whoami(); + // This means it was okay whoami, but no uat attached. + assert!(pre_res.unwrap().is_none()); // Now login as anonymous - - // Setup the auth initialisation - let auth_init = AuthRequest { - step: AuthStep::Init("anonymous".to_string(), None), - }; - - let mut response = client - .post(auth_dest.as_str()) - .body(serde_json::to_string(&auth_init).unwrap()) - .send() - .unwrap(); - assert!(response.status() == reqwest::StatusCode::OK); - // Check that we got the next step - let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); - println!("==> AUTHRESPONSE ==> {:?}", r); - - assert!(match &r.state { - AuthState::Continue(_all_list) => { - // Check anonymous is present? It will fail on next step if not ... - true - } - _ => false, - }); - - // Send the credentials required now - let auth_anon = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Anonymous]), - }; - - let mut response = client - .post(auth_dest.as_str()) - .body(serde_json::to_string(&auth_anon).unwrap()) - .send() - .unwrap(); - debug!("{}", response.status()); - assert!(response.status() == reqwest::StatusCode::OK); - // Check that we got the next step - let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); - println!("==> AUTHRESPONSE ==> {:?}", r); - - assert!(match &r.state { - AuthState::Success(uat) => { - println!("==> Authed as uat; {:?}", uat); - true - } - _ => false, - }); + let res = rsclient.auth_anonymous(); + assert!(res.is_ok()); // Now do a whoami. - let mut response = client.get(whoami_dest.as_str()).send().unwrap(); - println!("WHOAMI -> {}", response.text().unwrap().as_str()); - println!("WHOAMI STATUS -> {}", response.status()); - assert!(response.status() == reqwest::StatusCode::OK); - - // Check the json now ... response.json() + let post_res = rsclient.whoami().unwrap(); + assert!(post_res.is_some()); + // TODO: Now unwrap and ensure anony + println!("{:?}", post_res); }); } #[test] fn test_server_whoami_admin_simple_password() { - run_test(|client: reqwest::Client, addr: &str| { + run_test(|rsclient: RsidmClient| { // First show we are un-authenticated. - let whoami_dest = format!("{}/v1/whoami", addr); - let auth_dest = format!("{}/v1/auth", addr); - // Now login as admin + let pre_res = rsclient.whoami(); + // This means it was okay whoami, but no uat attached. + assert!(pre_res.unwrap().is_none()); - // Setup the auth initialisation - let auth_init = AuthRequest { - step: AuthStep::Init("admin".to_string(), None), - }; - - let mut response = client - .post(auth_dest.as_str()) - .body(serde_json::to_string(&auth_init).unwrap()) - .send() - .unwrap(); - assert!(response.status() == reqwest::StatusCode::OK); - // Check that we got the next step - let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); - println!("==> AUTHRESPONSE ==> {:?}", r); - - assert!(match &r.state { - AuthState::Continue(_all_list) => { - // Check anonymous is present? It will fail on next step if not ... - true - } - _ => false, - }); - - // Send the credentials required now - let auth_admin = AuthRequest { - step: AuthStep::Creds(vec![AuthCredential::Password( - ADMIN_TEST_PASSWORD.to_string(), - )]), - }; - - let mut response = client - .post(auth_dest.as_str()) - .body(serde_json::to_string(&auth_admin).unwrap()) - .send() - .unwrap(); - debug!("{}", response.status()); - assert!(response.status() == reqwest::StatusCode::OK); - // Check that we got the next step - let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); - println!("==> AUTHRESPONSE ==> {:?}", r); - - assert!(match &r.state { - AuthState::Success(uat) => { - println!("==> Authed as uat; {:?}", uat); - true - } - _ => false, - }); + let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD); + assert!(res.is_ok()); // Now do a whoami. - let mut response = client.get(whoami_dest.as_str()).send().unwrap(); - println!("WHOAMI -> {}", response.text().unwrap().as_str()); - println!("WHOAMI STATUS -> {}", response.status()); - assert!(response.status() == reqwest::StatusCode::OK); - - // Check the json now ... response.json() + let post_res = rsclient.whoami().unwrap(); + assert!(post_res.is_some()); + // TODO: Now unwrap and ensure anony + debug!("{:?}", post_res); }); } diff --git a/rsidm_proto/Cargo.toml b/rsidm_proto/Cargo.toml index c0a77a3a6..468ce645d 100644 --- a/rsidm_proto/Cargo.toml +++ b/rsidm_proto/Cargo.toml @@ -4,9 +4,6 @@ version = "0.1.0" authors = ["William Brown "] edition = "2018" -[features] -rsidm_internal = ["actix"] - [dependencies] serde = "1.0" serde_derive = "1.0" diff --git a/rsidm_proto/src/v1.rs b/rsidm_proto/src/v1.rs index 1f490c8f6..98caedd04 100644 --- a/rsidm_proto/src/v1.rs +++ b/rsidm_proto/src/v1.rs @@ -1,9 +1,7 @@ use std::collections::BTreeMap; +use std::fmt; use uuid::Uuid; -#[cfg(feature = "rsidm_internal")] -use actix::prelude::*; - // These proto implementations are here because they have public definitions /* ===== errors ===== */ @@ -120,6 +118,16 @@ pub struct UserAuthToken { // Should we allow supplemental ava's to be added on request? } +impl fmt::Display for UserAuthToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "name: {}", self.name)?; + writeln!(f, "display: {}", self.displayname)?; + writeln!(f, "uuid: {}", self.uuid)?; + writeln!(f, "groups: {:?}", self.groups)?; + writeln!(f, "claims: {:?}", self.claims) + } +} + // UAT will need a downcast to Entry, which adds in the claims to the entry // for the purpose of filtering. @@ -178,23 +186,14 @@ impl OperationResponse { #[derive(Debug, Serialize, Deserialize)] pub struct SearchRequest { pub filter: Filter, - pub user_uuid: String, } impl SearchRequest { - pub fn new(filter: Filter, user_uuid: &str) -> Self { - SearchRequest { - filter: filter, - user_uuid: user_uuid.to_string(), - } + pub fn new(filter: Filter) -> Self { + SearchRequest { filter: filter } } } -#[cfg(feature = "rsidm_internal")] -impl Message for SearchRequest { - type Result = Result; -} - #[derive(Debug, Serialize, Deserialize)] pub struct SearchResponse { pub entries: Vec, @@ -209,66 +208,41 @@ impl SearchResponse { #[derive(Debug, Serialize, Deserialize)] pub struct CreateRequest { pub entries: Vec, - pub user_uuid: String, } impl CreateRequest { - pub fn new(entries: Vec, user_uuid: &str) -> Self { - CreateRequest { - entries: entries, - user_uuid: user_uuid.to_string(), - } + pub fn new(entries: Vec) -> Self { + CreateRequest { entries: entries } } } -#[cfg(feature = "rsidm_internal")] -impl Message for CreateRequest { - type Result = Result; -} - #[derive(Debug, Serialize, Deserialize)] pub struct DeleteRequest { pub filter: Filter, - pub user_uuid: String, } impl DeleteRequest { - pub fn new(filter: Filter, user_uuid: &str) -> Self { - DeleteRequest { - filter: filter, - user_uuid: user_uuid.to_string(), - } + pub fn new(filter: Filter) -> Self { + DeleteRequest { filter: filter } } } -#[cfg(feature = "rsidm_internal")] -impl Message for DeleteRequest { - type Result = Result; -} - #[derive(Debug, Serialize, Deserialize)] pub struct ModifyRequest { // Probably needs a modlist? pub filter: Filter, pub modlist: ModifyList, - pub user_uuid: String, } impl ModifyRequest { - pub fn new(filter: Filter, modlist: ModifyList, user_uuid: &str) -> Self { + pub fn new(filter: Filter, modlist: ModifyList) -> Self { ModifyRequest { filter: filter, modlist: modlist, - user_uuid: user_uuid.to_string(), } } } -#[cfg(feature = "rsidm_internal")] -impl Message for ModifyRequest { - type Result = Result; -} - // Login is a multi-step process potentially. First the client says who they // want to request // @@ -340,15 +314,11 @@ pub struct AuthResponse { pub struct SearchRecycledRequest { pub filter: Filter, - pub user_uuid: String, } impl SearchRecycledRequest { - pub fn new(filter: Filter, user_uuid: &str) -> Self { - SearchRecycledRequest { - filter: filter, - user_uuid: user_uuid.to_string(), - } + pub fn new(filter: Filter) -> Self { + SearchRecycledRequest { filter: filter } } } @@ -356,15 +326,11 @@ impl SearchRecycledRequest { pub struct ReviveRecycledRequest { pub filter: Filter, - pub user_uuid: String, } impl ReviveRecycledRequest { - pub fn new(filter: Filter, user_uuid: &str) -> Self { - ReviveRecycledRequest { - filter: filter, - user_uuid: user_uuid.to_string(), - } + pub fn new(filter: Filter) -> Self { + ReviveRecycledRequest { filter: filter } } } @@ -382,11 +348,15 @@ impl WhoamiRequest { pub struct WhoamiResponse { // Should we just embed the entry? Or destructure it? pub youare: Entry, + pub uat: UserAuthToken, } impl WhoamiResponse { - pub fn new(e: Entry) -> Self { - WhoamiResponse { youare: e } + pub fn new(e: Entry, uat: UserAuthToken) -> Self { + WhoamiResponse { + youare: e, + uat: uat, + } } } diff --git a/rsidm_tools/Cargo.toml b/rsidm_tools/Cargo.toml index 537f7e024..47d07c7ed 100644 --- a/rsidm_tools/Cargo.toml +++ b/rsidm_tools/Cargo.toml @@ -10,4 +10,6 @@ path = "src/main.rs" [dependencies] rsidm_client = { path = "../rsidm_client" } +rpassword = "0.4" +structopt = { version = "0.2", default-features = false } diff --git a/rsidm_tools/src/main.rs b/rsidm_tools/src/main.rs index 44401251e..22394c130 100644 --- a/rsidm_tools/src/main.rs +++ b/rsidm_tools/src/main.rs @@ -1,3 +1,54 @@ -fn main() { - println!("Hello kanidm"); +extern crate structopt; +use rsidm_client::RsidmClient; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +struct CommonOpt { + #[structopt(short = "H", long = "url")] + addr: String, + #[structopt(short = "D", long = "name")] + username: String, +} + +impl CommonOpt { + fn to_client(&self) -> RsidmClient { + RsidmClient::new(self.addr.as_str()) + } +} + +#[derive(Debug, StructOpt)] +enum ClientOpt { + #[structopt(name = "whoami")] + Whoami(CommonOpt), +} + +fn main() { + let opt = ClientOpt::from_args(); + + match opt { + ClientOpt::Whoami(copt) => { + let client = copt.to_client(); + let r = if copt.username == "anonymous" { + client.auth_anonymous() + } else { + let password = rpassword::prompt_password_stderr("Enter password: ").unwrap(); + client.auth_simple_password(copt.username.as_str(), password.as_str()) + }; + + if r.is_err() { + println!("Error during authentication phase: {:?}", r); + return; + } + + match client.whoami() { + Ok(o_ent) => match o_ent { + Some((_ent, uat)) => { + println!("{}", uat); + } + None => println!("Unauthenticated"), + }, + Err(e) => println!("Error: {:?}", e), + } + } + } } diff --git a/rsidm_tools/src/whoami.rs b/rsidm_tools/src/whoami.rs deleted file mode 100644 index 97949b50a..000000000 --- a/rsidm_tools/src/whoami.rs +++ /dev/null @@ -1,28 +0,0 @@ -extern crate reqwest; -extern crate rsidm; - -use rsidm::proto::v1::{WhoamiRequest, WhoamiResponse}; - -fn main() { - println!("Hello whoami"); - - // Given the current ~/.rsidm/cookie (or none) - // we should check who we are plus show the auth token that the server - // would generate for us. - - let whoami_req = WhoamiRequest {}; - - // FIXME TODO: Make this url configurable!!! - let client = reqwest::Client::new(); - - let mut response = client - .get("http://127.0.0.1:8080/v1/whoami") - .send() - .unwrap(); - - println!("{:?}", response); - - // Parse it if desire. - // let r: Response = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); - // println!("{:?}", r); -} diff --git a/rsidmd/Cargo.toml b/rsidmd/Cargo.toml index 1da57c34e..471f17f47 100644 --- a/rsidmd/Cargo.toml +++ b/rsidmd/Cargo.toml @@ -17,7 +17,7 @@ path = "src/server/main.rs" [dependencies] -rsidm_proto = { path = "../rsidm_proto", features = ["rsidm_internal"] } +rsidm_proto = { path = "../rsidm_proto" } actix = "0.7" actix-web = "0.7" diff --git a/rsidmd/src/lib/access.rs b/rsidmd/src/lib/access.rs index c3c64b7bd..c61afce32 100644 --- a/rsidmd/src/lib/access.rs +++ b/rsidmd/src/lib/access.rs @@ -858,7 +858,7 @@ pub trait AccessControlsTransaction { }) .collect(); - audit_log!(audit, "Related acs -> {:?}", related_acp); + audit_log!(audit, "Related acc -> {:?}", related_acp); // For each entry let r = entries.iter().fold(true, |acc, e| { diff --git a/rsidmd/src/lib/actors/v1.rs b/rsidmd/src/lib/actors/v1.rs index 567af6ea0..d9a6f5437 100644 --- a/rsidmd/src/lib/actors/v1.rs +++ b/rsidmd/src/lib/actors/v1.rs @@ -61,6 +61,66 @@ impl Message for AuthMessage { type Result = Result; } +pub struct CreateMessage { + pub uat: Option, + pub req: CreateRequest, +} + +impl CreateMessage { + pub fn new(uat: Option, req: CreateRequest) -> Self { + CreateMessage { uat: uat, req: req } + } +} + +impl Message for CreateMessage { + type Result = Result; +} + +pub struct DeleteMessage { + pub uat: Option, + pub req: DeleteRequest, +} + +impl DeleteMessage { + pub fn new(uat: Option, req: DeleteRequest) -> Self { + DeleteMessage { uat: uat, req: req } + } +} + +impl Message for DeleteMessage { + type Result = Result; +} + +pub struct ModifyMessage { + pub uat: Option, + pub req: ModifyRequest, +} + +impl ModifyMessage { + pub fn new(uat: Option, req: ModifyRequest) -> Self { + ModifyMessage { uat: uat, req: req } + } +} + +impl Message for ModifyMessage { + type Result = Result; +} + +pub struct SearchMessage { + pub uat: Option, + pub req: SearchRequest, +} + +impl SearchMessage { + pub fn new(uat: Option, req: SearchRequest) -> Self { + SearchMessage { uat: uat, req: req } + } +} + +impl Message for SearchMessage { + type Result = Result; +} + pub struct QueryServerV1 { log: actix::Addr, qs: QueryServer, @@ -98,22 +158,22 @@ impl QueryServerV1 { } } -// The server only recieves "Event" structures, which +// The server only recieves "Message" structures, which // are whole self contained DB operations with all parsing // required complete. We still need to do certain validation steps, but // at this point our just is just to route to do_ -impl Handler for QueryServerV1 { +impl Handler for QueryServerV1 { type Result = Result; - fn handle(&mut self, msg: SearchRequest, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: SearchMessage, _: &mut Self::Context) -> Self::Result { let mut audit = AuditScope::new("search"); let res = audit_segment!(&mut audit, || { // Begin a read let qs_read = self.qs.read(); // Make an event from the request - let srch = match SearchEvent::from_request(&mut audit, msg, &qs_read) { + let srch = match SearchEvent::from_message(&mut audit, msg, &qs_read) { Ok(s) => s, Err(e) => { audit_log!(audit, "Failed to begin search: {:?}", e); @@ -138,15 +198,15 @@ impl Handler for QueryServerV1 { } } -impl Handler for QueryServerV1 { +impl Handler for QueryServerV1 { type Result = Result; - fn handle(&mut self, msg: CreateRequest, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: CreateMessage, _: &mut Self::Context) -> Self::Result { let mut audit = AuditScope::new("create"); let res = audit_segment!(&mut audit, || { let mut qs_write = self.qs.write(); - let crt = match CreateEvent::from_request(&mut audit, msg, &qs_write) { + let crt = match CreateEvent::from_message(&mut audit, msg, &qs_write) { Ok(c) => c, Err(e) => { audit_log!(audit, "Failed to begin create: {:?}", e); @@ -166,14 +226,14 @@ impl Handler for QueryServerV1 { } } -impl Handler for QueryServerV1 { +impl Handler for QueryServerV1 { type Result = Result; - fn handle(&mut self, msg: ModifyRequest, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ModifyMessage, _: &mut Self::Context) -> Self::Result { let mut audit = AuditScope::new("modify"); let res = audit_segment!(&mut audit, || { let mut qs_write = self.qs.write(); - let mdf = match ModifyEvent::from_request(&mut audit, msg, &qs_write) { + let mdf = match ModifyEvent::from_message(&mut audit, msg, &qs_write) { Ok(m) => m, Err(e) => { audit_log!(audit, "Failed to begin modify: {:?}", e); @@ -192,15 +252,15 @@ impl Handler for QueryServerV1 { } } -impl Handler for QueryServerV1 { +impl Handler for QueryServerV1 { type Result = Result; - fn handle(&mut self, msg: DeleteRequest, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: DeleteMessage, _: &mut Self::Context) -> Self::Result { let mut audit = AuditScope::new("delete"); let res = audit_segment!(&mut audit, || { let mut qs_write = self.qs.write(); - let del = match DeleteEvent::from_request(&mut audit, msg, &qs_write) { + let del = match DeleteEvent::from_message(&mut audit, msg, &qs_write) { Ok(d) => d, Err(e) => { audit_log!(audit, "Failed to begin delete: {:?}", e); @@ -285,6 +345,8 @@ impl Handler for QueryServerV1 { // trigger the failure, but if we can manage to work out async // then move this to core.rs, and don't allow Option to get // this far. + let uat = msg.uat.clone().ok_or(OperationError::NotAuthenticated)?; + let srch = match SearchEvent::from_whoami_request(&mut audit, msg.uat, &qs_read) { Ok(s) => s, Err(e) => { @@ -303,7 +365,7 @@ impl Handler for QueryServerV1 { 1 => { let e = entries.pop().expect("Entry length mismatch!!!"); // Now convert to a response, and return - let wr = WhoamiResult::new(e); + let wr = WhoamiResult::new(e, uat); Ok(wr.response()) } // Somehow we matched multiple, which should be impossible. diff --git a/rsidmd/src/lib/config.rs b/rsidmd/src/lib/config.rs index 12c8ca2d1..17770bdcc 100644 --- a/rsidmd/src/lib/config.rs +++ b/rsidmd/src/lib/config.rs @@ -32,7 +32,8 @@ impl Configuration { // log type // log path // TODO #63: default true in prd - secure_cookies: if cfg!(test) { false } else { true }, + // secure_cookies: if cfg!(test) { false } else { true }, + secure_cookies: false, cookie_key: [0; 32], server_id: [0; 4], integration_test_config: None, diff --git a/rsidmd/src/lib/constants.rs b/rsidmd/src/lib/constants.rs index 351249040..240c4e5ff 100644 --- a/rsidmd/src/lib/constants.rs +++ b/rsidmd/src/lib/constants.rs @@ -24,7 +24,7 @@ pub static JSON_ADMIN_V1: &'static str = r#"{ }, "state": null, "attrs": { - "class": ["account", "object"], + "class": ["account", "memberof", "object"], "name": ["admin"], "uuid": ["00000000-0000-0000-0000-000000000000"], "description": ["Builtin Admin account."], @@ -80,7 +80,7 @@ pub static JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{ "acp_targetscope": [ "{\"Pres\":\"class\"}" ], - "acp_search_attr": ["name", "class", "uuid"] + "acp_search_attr": ["name", "class", "uuid", "description", "displayname"] } }"#; @@ -129,6 +129,40 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{ } }"#; +pub static _UUID_IDM_ADMINS_ACP_MANAGE_V1: &'static str = "00000000-0000-0000-0000-ffffff000005"; +pub static JSON_IDM_ADMINS_ACP_MANAGE_V1: &'static str = r#"{ + "valid": { + "uuid": "00000000-0000-0000-0000-ffffff000005" + }, + "state": null, + "attrs": { + "class": [ + "object", + "access_control_profile", + "access_control_modify", + "access_control_create", + "access_control_delete", + "access_control_search" + ], + "name": ["idm_admins_acp_manage"], + "uuid": ["00000000-0000-0000-0000-ffffff000005"], + "description": ["Builtin IDM Administrators Access Controls to manage the install."], + "acp_enable": ["true"], + "acp_receiver": [ + "{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000001\"]}" + ], + "acp_targetscope": [ + "{\"Pres\":\"class\"}" + ], + "acp_search_attr": ["name", "class", "uuid", "classname", "attributename"], + "acp_modify_class": ["person"], + "acp_modify_removedattr": ["class", "displayname", "name", "description"], + "acp_modify_presentattr": ["class", "displayname", "name", "description"], + "acp_create_class": ["object", "person", "account"], + "acp_create_attr": ["name", "class", "description", "displayname"] + } +}"#; + pub static JSON_ANONYMOUS_V1: &'static str = r#"{ "valid": { "uuid": "00000000-0000-0000-0000-ffffffffffff" diff --git a/rsidmd/src/lib/core.rs b/rsidmd/src/lib/core.rs index 8a3550e72..8f1102bad 100644 --- a/rsidmd/src/lib/core.rs +++ b/rsidmd/src/lib/core.rs @@ -13,7 +13,9 @@ use crate::config::Configuration; // SearchResult use crate::actors::v1::QueryServerV1; -use crate::actors::v1::{AuthMessage, WhoamiMessage}; +use crate::actors::v1::{ + AuthMessage, CreateMessage, DeleteMessage, ModifyMessage, SearchMessage, WhoamiMessage, +}; use crate::async_log; use crate::audit::AuditScope; use crate::be::{Backend, BackendTransaction}; @@ -46,12 +48,15 @@ fn get_current_user(req: &HttpRequest) -> Option { } macro_rules! json_event_post { - ($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ + ($req:expr, $state:expr, $message_type:ty, $request_type:ty) => {{ // This is copied every request. Is there a better way? // The issue is the fold move takes ownership of state if // we don't copy this here let max_size = $state.max_size; + // Get auth if any? + let uat = get_current_user(&$req); + // HttpRequest::payload() is stream of Bytes objects $req.payload() // `Future::from_err` acts like `?` in that it coerces the error type from @@ -73,20 +78,17 @@ macro_rules! json_event_post { .and_then( move |body| -> Box> { // body is loaded, now we can deserialize serde-json - // let r_obj = serde_json::from_slice::(&body); - let r_obj = serde_json::from_slice::<$message_type>(&body); + let r_obj = serde_json::from_slice::<$request_type>(&body); // Send to the db for handling match r_obj { Ok(obj) => { + // combine request + uat -> message. + let m_obj = <($message_type)>::new(uat, obj); let res = $state .qe - .send( - // Could make this a .into_inner() and move? - // event::SearchEvent::new(obj.filter), - // <($event_type)>::from_request(obj), - obj, - ) + .send(m_obj) + // What is from_err? .from_err() .and_then(|res| match res { Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), @@ -106,7 +108,7 @@ macro_rules! json_event_post { } macro_rules! json_event_get { - ($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ + ($req:expr, $state:expr, $message_type:ty) => {{ // Get current auth data - remember, the QS checks if the // none/some is okay, because it's too hard to make it work here // with all the async parts. @@ -132,32 +134,31 @@ macro_rules! json_event_get { fn create( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_post!(req, state, CreateEvent, CreateRequest) + json_event_post!(req, state, CreateMessage, CreateRequest) } fn modify( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_post!(req, state, ModifyEvent, ModifyRequest) + json_event_post!(req, state, ModifyMessage, ModifyRequest) } fn delete( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_post!(req, state, DeleteEvent, DeleteRequest) + json_event_post!(req, state, DeleteMessage, DeleteRequest) } fn search( (req, state): (HttpRequest, State), ) -> impl Future { - json_event_post!(req, state, SearchEvent, SearchRequest) + json_event_post!(req, state, SearchMessage, SearchRequest) } fn whoami( (req, state): (HttpRequest, State), ) -> impl Future { - // Actually this may not work as it assumes post not get. - json_event_get!(req, state, WhoamiEvent, WhoamiMessage) + json_event_get!(req, state, WhoamiMessage) } // We probably need an extract auth or similar to handle the different diff --git a/rsidmd/src/lib/event.rs b/rsidmd/src/lib/event.rs index 212102edc..5e2487ce8 100644 --- a/rsidmd/src/lib/event.rs +++ b/rsidmd/src/lib/event.rs @@ -3,8 +3,8 @@ use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, use crate::filter::{Filter, FilterValid}; use rsidm_proto::v1::Entry as ProtoEntry; use rsidm_proto::v1::{ - AuthCredential, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, ModifyRequest, - ReviveRecycledRequest, SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse, + AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken, + WhoamiResponse, }; // use error::OperationError; use crate::modify::{ModifyList, ModifyValid}; @@ -13,7 +13,7 @@ use crate::server::{ }; use rsidm_proto::v1::OperationError; -use crate::actors::v1::AuthMessage; +use crate::actors::v1::{AuthMessage, CreateMessage, DeleteMessage, ModifyMessage, SearchMessage}; // Bring in schematransaction trait for validate // use crate::schema::SchemaTransaction; @@ -22,8 +22,6 @@ use crate::actors::v1::AuthMessage; use crate::filter::FilterInvalid; #[cfg(test)] use crate::modify::ModifyInvalid; -#[cfg(test)] -use rsidm_proto::v1::SearchRecycledRequest; use actix::prelude::*; use uuid::Uuid; @@ -120,6 +118,27 @@ impl Event { }) } + pub fn from_rw_uat( + audit: &mut AuditScope, + qs: &QueryServerWriteTransaction, + uat: Option, + ) -> Result { + audit_log!(audit, "from_rw_uat -> {:?}", uat); + let uat = uat.ok_or(OperationError::NotAuthenticated)?; + let u = try_audit!( + audit, + Uuid::parse_str(uat.uuid.as_str()).map_err(|_| OperationError::InvalidUuid) + ); + + let e = try_audit!(audit, qs.internal_search_uuid(audit, &u)); + // TODO #64: Now apply claims from the uat into the Entry + // to allow filtering. + + Ok(Event { + origin: EventOrigin::User(e), + }) + } + pub fn from_rw_request( audit: &mut AuditScope, qs: &QueryServerWriteTransaction, @@ -186,14 +205,14 @@ pub struct SearchEvent { } impl SearchEvent { - pub fn from_request( + pub fn from_message( audit: &mut AuditScope, - request: SearchRequest, + msg: SearchMessage, qs: &QueryServerReadTransaction, ) -> Result { - match Filter::from_ro(audit, &request.filter, qs) { + match Filter::from_ro(audit, &msg.req.filter, qs) { Ok(f) => Ok(SearchEvent { - event: Event::from_ro_request(audit, qs, request.user_uuid.as_str())?, + event: Event::from_ro_uat(audit, qs, msg.uat)?, // We do need to do this twice to account for the ignore_hidden // changes. filter: f @@ -259,6 +278,7 @@ impl SearchEvent { } } + /* #[cfg(test)] #[allow(dead_code)] pub fn from_rec_request( @@ -268,7 +288,7 @@ impl SearchEvent { ) -> Result { match Filter::from_ro(audit, &request.filter, qs) { Ok(f) => Ok(SearchEvent { - event: Event::from_ro_request(audit, qs, request.user_uuid.as_str())?, + event: Event::from_ro_uat(audit, qs, msg.uat)?, filter: f .clone() .to_recycled() @@ -281,6 +301,7 @@ impl SearchEvent { Err(e) => Err(e), } } + */ #[cfg(test)] /* Impersonate a request for recycled objects */ @@ -340,12 +361,13 @@ pub struct CreateEvent { } impl CreateEvent { - pub fn from_request( + pub fn from_message( audit: &mut AuditScope, - request: CreateRequest, + msg: CreateMessage, qs: &QueryServerWriteTransaction, ) -> Result { - let rentries: Result, _> = request + let rentries: Result, _> = msg + .req .entries .iter() .map(|e| Entry::from_proto_entry(audit, e, qs)) @@ -355,7 +377,7 @@ impl CreateEvent { // From ProtoEntry -> Entry // What is the correct consuming iterator here? Can we // even do that? - event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, + event: Event::from_rw_uat(audit, qs, msg.uat)?, entries: entries, }), Err(e) => Err(e), @@ -421,14 +443,14 @@ pub struct DeleteEvent { } impl DeleteEvent { - pub fn from_request( + pub fn from_message( audit: &mut AuditScope, - request: DeleteRequest, + msg: DeleteMessage, qs: &QueryServerWriteTransaction, ) -> Result { - match Filter::from_rw(audit, &request.filter, qs) { + match Filter::from_rw(audit, &msg.req.filter, qs) { Ok(f) => Ok(DeleteEvent { - event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, + event: Event::from_rw_uat(audit, qs, msg.uat)?, filter: f .clone() .to_ignore_hidden() @@ -442,6 +464,18 @@ impl DeleteEvent { } } + #[cfg(test)] + pub unsafe fn new_impersonate_entry( + e: Entry, + filter: Filter, + ) -> Self { + DeleteEvent { + event: Event::from_impersonate_entry(e), + filter: filter.clone().to_valid(), + filter_orig: filter.to_valid(), + } + } + #[cfg(test)] pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter) -> Self { DeleteEvent { @@ -480,15 +514,15 @@ pub struct ModifyEvent { } impl ModifyEvent { - pub fn from_request( + pub fn from_message( audit: &mut AuditScope, - request: ModifyRequest, + msg: ModifyMessage, qs: &QueryServerWriteTransaction, ) -> Result { - match Filter::from_rw(audit, &request.filter, qs) { - Ok(f) => match ModifyList::from(audit, &request.modlist, qs) { + match Filter::from_rw(audit, &msg.req.filter, qs) { + Ok(f) => match ModifyList::from(audit, &msg.req.modlist, qs) { Ok(m) => Ok(ModifyEvent { - event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, + event: Event::from_rw_uat(audit, qs, msg.uat)?, filter: f .clone() .to_ignore_hidden() @@ -544,6 +578,20 @@ impl ModifyEvent { } } + #[cfg(test)] + pub unsafe fn new_impersonate_entry( + e: Entry, + filter: Filter, + modlist: ModifyList, + ) -> Self { + ModifyEvent { + event: Event::from_impersonate_entry(e), + filter: filter.clone().to_valid(), + filter_orig: filter.to_valid(), + modlist: modlist.to_valid(), + } + } + pub fn new_impersonate( event: &Event, filter: Filter, @@ -703,18 +751,21 @@ impl AuthResult { pub struct WhoamiResult { youare: ProtoEntry, + uat: UserAuthToken, } impl WhoamiResult { - pub fn new(e: Entry) -> Self { + pub fn new(e: Entry, uat: UserAuthToken) -> Self { WhoamiResult { youare: e.into_pe(), + uat: uat, } } pub fn response(self) -> WhoamiResponse { WhoamiResponse { youare: self.youare, + uat: self.uat, } } } @@ -769,14 +820,15 @@ impl Message for ReviveRecycledEvent { } impl ReviveRecycledEvent { - pub fn from_request( + /* + pub fn from_message( audit: &mut AuditScope, - request: ReviveRecycledRequest, + msg: ReviveRecycledMessage, qs: &QueryServerWriteTransaction, ) -> Result { - match Filter::from_rw(audit, &request.filter, qs) { + match Filter::from_rw(audit, &msg.req.filter, qs) { Ok(f) => Ok(ReviveRecycledEvent { - event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, + event: Event::from_rw_uat(audit, qs, msg.uat)?, filter: f .to_recycled() .validate(qs.get_schema()) @@ -785,4 +837,16 @@ impl ReviveRecycledEvent { Err(e) => Err(e), } } + */ + + #[cfg(test)] + pub unsafe fn new_impersonate_entry( + e: Entry, + filter: Filter, + ) -> Self { + ReviveRecycledEvent { + event: Event::from_impersonate_entry(e), + filter: filter.to_valid(), + } + } } diff --git a/rsidmd/src/lib/lib.rs b/rsidmd/src/lib/lib.rs index 68b4f8d00..baea23c29 100644 --- a/rsidmd/src/lib/lib.rs +++ b/rsidmd/src/lib/lib.rs @@ -1,4 +1,4 @@ -// #![deny(warnings)] +#![deny(warnings)] #![warn(unused_extern_crates)] #[macro_use] diff --git a/rsidmd/src/lib/plugins/protected.rs b/rsidmd/src/lib/plugins/protected.rs index 509d144c7..6bca631aa 100644 --- a/rsidmd/src/lib/plugins/protected.rs +++ b/rsidmd/src/lib/plugins/protected.rs @@ -25,7 +25,11 @@ lazy_static! { 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 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"); } impl Plugin for Protected { @@ -51,7 +55,10 @@ impl Plugin for Protected { cand.iter().fold(Ok(()), |acc, cand| match acc { Err(_) => acc, Ok(_) => { - if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) { + if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) + || cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE) + || cand.attribute_value_pres("class", &PVCLASS_RECYCLED) + { Err(OperationError::SystemProtectedObject) } else { acc @@ -74,7 +81,7 @@ impl Plugin for Protected { ); return Ok(()); } - // Prevent adding class: system + // Prevent adding class: system, tombstone, or recycled. me.modlist.iter().fold(Ok(()), |acc, m| { if acc.is_err() { acc @@ -82,7 +89,11 @@ impl Plugin for Protected { match m { Modify::Present(a, v) => { // TODO: Can we avoid this clone? - if a == "class" && v == &(VCLASS_SYSTEM.clone()) { + if a == "class" + && (v == &(VCLASS_SYSTEM.clone()) + || v == &(VCLASS_TOMBSTONE.clone()) + || v == &(VCLASS_RECYCLED.clone())) + { Err(OperationError::SystemProtectedObject) } else { Ok(()) @@ -92,8 +103,22 @@ impl Plugin for Protected { } } })?; - // if class: system, check the mods are "allowed" + // HARD block mods on tombstone or recycle. + cand.iter().fold(Ok(()), |acc, cand| match acc { + Err(_) => acc, + Ok(_) => { + if cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE) + || cand.attribute_value_pres("class", &PVCLASS_RECYCLED) + { + Err(OperationError::SystemProtectedObject) + } else { + acc + } + } + })?; + + // if class: system, check the mods are "allowed" let system_pres = cand.iter().fold(false, |acc, c| { if acc { acc @@ -145,7 +170,10 @@ impl Plugin for Protected { cand.iter().fold(Ok(()), |acc, cand| match acc { Err(_) => acc, Ok(_) => { - if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) { + if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) + || cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE) + || cand.attribute_value_pres("class", &PVCLASS_RECYCLED) + { Err(OperationError::SystemProtectedObject) } else { acc diff --git a/rsidmd/src/lib/server.rs b/rsidmd/src/lib/server.rs index f4cca26e2..08406b1f8 100644 --- a/rsidmd/src/lib/server.rs +++ b/rsidmd/src/lib/server.rs @@ -13,11 +13,11 @@ use crate::access::{ AccessControlsWriteTransaction, }; use crate::constants::{ - JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1, - JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, JSON_SCHEMA_ATTR_DISPLAYNAME, - JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, - JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_PERSON, - JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST, + JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_MANAGE_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, + JSON_IDM_ADMINS_ACP_SEARCH_V1, JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, + JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, + JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP, + JSON_SCHEMA_CLASS_PERSON, JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST, }; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid}; use crate::event::{ @@ -944,6 +944,11 @@ impl<'a> QueryServerWriteTransaction<'a> { Err(e) => return Err(e), }; + if ts.len() == 0 { + audit_log!(au, "No Tombstones present - purge operation success"); + return Ok(()); + } + // TODO #68: Has an appropriate amount of time/condition past (ie replication events?) // Delete them @@ -975,6 +980,11 @@ impl<'a> QueryServerWriteTransaction<'a> { Err(e) => return Err(e), }; + if rc.len() == 0 { + audit_log!(au, "No recycled present - purge operation success"); + return Ok(()); + } + // Modify them to strip all avas except uuid let tombstone_cand = rc.iter().map(|e| e.to_tombstone()).collect(); @@ -1514,6 +1524,9 @@ impl<'a> QueryServerWriteTransaction<'a> { .and_then(|_| { self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1) }) + .and_then(|_| { + self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_MANAGE_V1) + }) .and_then(|_| { self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1) }); @@ -1689,17 +1702,13 @@ impl<'a> QueryServerWriteTransaction<'a> { #[cfg(test)] mod tests { - use crate::constants::{JSON_ADMIN_V1, STR_UUID_ADMIN, UUID_ADMIN}; + use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN}; use crate::credential::Credential; use crate::entry::{Entry, EntryInvalid, EntryNew}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent}; use crate::modify::{Modify, ModifyList}; use crate::server::QueryServerTransaction; use crate::value::{PartialValue, Value}; - use rsidm_proto::v1::Filter as ProtoFilter; - use rsidm_proto::v1::Modify as ProtoModify; - use rsidm_proto::v1::ModifyList as ProtoModifyList; - use rsidm_proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest}; use rsidm_proto::v1::{OperationError, SchemaError}; use uuid::Uuid; @@ -2079,31 +2088,23 @@ mod tests { .internal_search_uuid(audit, &UUID_ADMIN) .expect("failed"); - let filt_ts = ProtoFilter::Eq("class".to_string(), "tombstone".to_string()); - let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone"))); // Create fake external requests. Probably from admin later // Should we do this with impersonate instead of using the external - let me_ts = ModifyEvent::from_request( - audit, - ModifyRequest::new( - filt_ts.clone(), - ProtoModifyList::new_list(vec![ProtoModify::Present( + let me_ts = unsafe { + ModifyEvent::new_impersonate_entry( + admin.clone(), + filt_i_ts.clone(), + ModifyList::new_list(vec![Modify::Present( "class".to_string(), - "tombstone".to_string(), + Value::new_class("tombstone"), )]), - STR_UUID_ADMIN, - ), - &server_txn, - ) - .expect("modify event create failed"); - let de_ts = DeleteEvent::from_request( - audit, - DeleteRequest::new(filt_ts.clone(), STR_UUID_ADMIN), - &server_txn, - ) - .expect("delete event create failed"); + ) + }; + + let de_ts = + unsafe { DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_ts.clone()) }; let se_ts = unsafe { SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone()) }; // First, create a tombstone @@ -2163,8 +2164,6 @@ mod tests { .internal_search_uuid(audit, &UUID_ADMIN) .expect("failed"); - let filt_rc = ProtoFilter::Eq("class".to_string(), "recycled".to_string()); - let filt_i_rc = filter_all!(f_eq("class", PartialValue::new_class("recycled"))); let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone"))); @@ -2172,40 +2171,32 @@ mod tests { let filt_i_per = filter_all!(f_eq("class", PartialValue::new_class("person"))); // Create fake external requests. Probably from admin later - let me_rc = ModifyEvent::from_request( - audit, - ModifyRequest::new( - filt_rc.clone(), - ProtoModifyList::new_list(vec![ProtoModify::Present( + let me_rc = unsafe { + ModifyEvent::new_impersonate_entry( + admin.clone(), + filt_i_rc.clone(), + ModifyList::new_list(vec![Modify::Present( "class".to_string(), - "recycled".to_string(), + Value::new_class("recycled"), )]), - STR_UUID_ADMIN, - ), - &server_txn, - ) - .expect("modify event create failed"); - let de_rc = DeleteEvent::from_request( - audit, - DeleteRequest::new(filt_rc.clone(), STR_UUID_ADMIN), - &server_txn, - ) - .expect("delete event create failed"); + ) + }; + + let de_rc = + unsafe { DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_rc.clone()) }; + let se_rc = unsafe { SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone()) }; let sre_rc = - unsafe { SearchEvent::new_rec_impersonate_entry(admin, filt_i_rc.clone()) }; + unsafe { SearchEvent::new_rec_impersonate_entry(admin.clone(), filt_i_rc.clone()) }; - let rre_rc = ReviveRecycledEvent::from_request( - audit, - ReviveRecycledRequest::new( - ProtoFilter::Eq("name".to_string(), "testperson1".to_string()), - STR_UUID_ADMIN, - ), - &server_txn, - ) - .expect("revive recycled create failed"); + let rre_rc = unsafe { + ReviveRecycledEvent::new_impersonate_entry( + admin, + filter_all!(f_eq("name", PartialValue::new_iutf8s("testperson1"))), + ) + }; // Create some recycled objects let e1: Entry = Entry::unsafe_from_entry_str(