From 6388bcf6fc3034ef92f6f5dade5ce4c282a97a0a Mon Sep 17 00:00:00 2001 From: Firstyear Date: Wed, 25 Mar 2020 08:21:49 +1000 Subject: [PATCH] 20200322 132 recyclebin 2 (#193) Implements #132, the recycle bin. This completes the feature, with working API's, front end tests and CLI tooling. It also includes a refactor of the CLI tools to make them a bit easier to manage/work with. --- kanidm_book/src/SUMMARY.md | 1 + kanidm_book/src/recycle_bin.md | 40 ++ kanidm_client/src/lib.rs | 13 + kanidm_client/tests/proto_v1_test.rs | 42 +- kanidm_proto/src/v1.rs | 1 + kanidm_tools/Cargo.toml | 6 +- kanidm_tools/src/cli/account.rs | 311 +++++++++++++ kanidm_tools/src/cli/common.rs | 70 +++ kanidm_tools/src/cli/group.rs | 132 ++++++ kanidm_tools/src/cli/lib.rs | 103 ++++ kanidm_tools/src/cli/main.rs | 15 + kanidm_tools/src/cli/raw.rs | 118 +++++ kanidm_tools/src/cli/recycle.rs | 46 ++ kanidm_tools/src/main.rs | 670 --------------------------- kanidmd/src/lib/actors/v1_read.rs | 44 ++ kanidmd/src/lib/actors/v1_write.rs | 38 ++ kanidmd/src/lib/constants/mod.rs | 21 +- kanidmd/src/lib/core/mod.rs | 56 ++- kanidmd/src/lib/event.rs | 87 ++-- kanidmd/src/lib/interval.rs | 6 +- kanidmd/src/lib/repl/cid.rs | 12 + kanidmd/src/lib/server.rs | 102 ++-- 22 files changed, 1183 insertions(+), 751 deletions(-) create mode 100644 kanidm_book/src/recycle_bin.md create mode 100644 kanidm_tools/src/cli/account.rs create mode 100644 kanidm_tools/src/cli/common.rs create mode 100644 kanidm_tools/src/cli/group.rs create mode 100644 kanidm_tools/src/cli/lib.rs create mode 100644 kanidm_tools/src/cli/main.rs create mode 100644 kanidm_tools/src/cli/raw.rs create mode 100644 kanidm_tools/src/cli/recycle.rs delete mode 100644 kanidm_tools/src/main.rs diff --git a/kanidm_book/src/SUMMARY.md b/kanidm_book/src/SUMMARY.md index d27425cf9..2cee935ed 100644 --- a/kanidm_book/src/SUMMARY.md +++ b/kanidm_book/src/SUMMARY.md @@ -10,6 +10,7 @@ - [SSH Key Distribution](./ssh_key_dist.md) - [RADIUS](./radius.md) - [Password Quality and Badlisting](./password_quality.md) +- [Recycle Bin](./recycle_bin.md) ----------- [Why TLS?](./why_tls.md) diff --git a/kanidm_book/src/recycle_bin.md b/kanidm_book/src/recycle_bin.md new file mode 100644 index 000000000..cf2594c8b --- /dev/null +++ b/kanidm_book/src/recycle_bin.md @@ -0,0 +1,40 @@ +# Recycle Bin + +The recycle bin is a storage of deleted entries from the server. This allows +recovery from mistakes for a period of time. + +> **WARNING:** The recycle bin is a best effort - when recovering in some cases +> not everything can be "put back" the way it was. Be sure to check your entries +> are sane once they have been revived. + +## Where is the Recycle Bin? + +The recycle bin is stored as part of your main database - it is included in all +backups and restores, just like any other data. It is also replicated between +all servers. + +## How do things get into the Recycle Bin? + +Any delete operation of an entry will cause it to be sent to the recycle bin. No +configuration or specification is required. + +## How long do items stay in the Recycle Bin? + +Currently they stay up to 1 week before they are removed. + +## Managing the Recycle Bin + +You can display all items in the Recycle Bin with: + + kanidm recycle_bin list --name admin + +You can show a single items with: + + kanidm recycle_bin get --name admin + +An entry can be revived with: + + kanidm recycle_bin revive --name admin + + + diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index bec6abf82..a9d030431 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -761,4 +761,17 @@ impl KanidmClient { pub fn idm_schema_classtype_get(&self, id: &str) -> Result, ClientError> { self.perform_get_request(format!("/v1/schema/classtype/{}", id).as_str()) } + + // ==== recycle bin + pub fn recycle_bin_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/recycle_bin") + } + + pub fn recycle_bin_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/recycle_bin/{}", id).as_str()) + } + + pub fn recycle_bin_revive(&self, id: &str) -> Result<(), ClientError> { + self.perform_post_request(format!("/v1/recycle_bin/{}/_revive", id).as_str(), ()) + } } diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index c83a7bb44..0f9b311a6 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -20,7 +20,7 @@ static UNIX_TEST_PASSWORD: &str = "unix test user password"; // Test external behaviorus of the service. fn run_test(test_fn: fn(KanidmClient) -> ()) { - // ::std::env::set_var("RUST_LOG", "actix_web=debug,kanidm=debug"); + ::std::env::set_var("RUST_LOG", "actix_web=debug,kanidm=debug"); let _ = env_logger::builder().is_test(true).try_init(); let (tx, rx) = mpsc::channel(); let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst); @@ -675,6 +675,46 @@ fn test_server_rest_posix_auth_lifecycle() { }); } +#[test] +fn test_server_rest_recycle_lifecycle() { + run_test(|rsclient: KanidmClient| { + let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD); + assert!(res.is_ok()); + + // Not recommended in production! + rsclient + .idm_group_add_members("idm_admins", vec!["admin"]) + .unwrap(); + + // Setup a unix user + rsclient + .idm_account_create("recycle_account", "Recycle Demo Account") + .unwrap(); + + // delete them + rsclient.idm_account_delete("recycle_account").unwrap(); + + // not there + let acc = rsclient.idm_account_get("recycle_account").unwrap(); + assert!(acc.is_none()); + + // list the recycle bin + let r_list = rsclient.recycle_bin_list().unwrap(); + + assert!(r_list.len() == 1); + // get the user in recycle bin + let r_user = rsclient.recycle_bin_get("recycle_account").unwrap(); + assert!(r_user.is_some()); + + // revive + rsclient.recycle_bin_revive("recycle_account").unwrap(); + + // they are there! + let acc = rsclient.idm_account_get("recycle_account").unwrap(); + assert!(acc.is_some()); + }); +} + // Test the self version of the radius path. // Test hitting all auth-required endpoints and assert they give unauthorized. diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index 2730a8249..eb5a5caff 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -61,6 +61,7 @@ pub enum OperationError { InvalidState, InvalidEntryState, InvalidUuid, + InvalidReplCID, InvalidACPState(String), InvalidSchemaState(String), InvalidAccountState(String), diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index 5f5f65dd3..43fbe44cc 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -10,9 +10,13 @@ documentation = "https://docs.rs/kanidm_tools/latest/kanidm_tools/" homepage = "https://github.com/kanidm/kanidm/" repository = "https://github.com/kanidm/kanidm/" +[lib] +name = "kanidm_cli" +path = "src/cli/lib.rs" + [[bin]] name = "kanidm" -path = "src/main.rs" +path = "src/cli/main.rs" [[bin]] name = "kanidm_ssh_authorizedkeys_direct" diff --git a/kanidm_tools/src/cli/account.rs b/kanidm_tools/src/cli/account.rs new file mode 100644 index 000000000..567ceed38 --- /dev/null +++ b/kanidm_tools/src/cli/account.rs @@ -0,0 +1,311 @@ +use crate::common::CommonOpt; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +pub struct AccountCommonOpt { + #[structopt()] + account_id: String, +} + +#[derive(Debug, StructOpt)] +pub struct AccountCredentialSet { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt()] + application_id: Option, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub struct AccountNamedOpt { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub struct AccountNamedTagOpt { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt(flatten)] + copt: CommonOpt, + #[structopt(name = "tag")] + tag: String, +} + +#[derive(Debug, StructOpt)] +pub struct AccountNamedTagPKOpt { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt(flatten)] + copt: CommonOpt, + #[structopt(name = "tag")] + tag: String, + #[structopt(name = "pubkey")] + pubkey: String, +} + +#[derive(Debug, StructOpt)] +pub struct AccountCreateOpt { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt(name = "display_name")] + display_name: String, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub enum AccountCredential { + #[structopt(name = "set_password")] + SetPassword(AccountCredentialSet), + #[structopt(name = "generate_password")] + GeneratePassword(AccountCredentialSet), +} + +#[derive(Debug, StructOpt)] +pub enum AccountRadius { + #[structopt(name = "show_secret")] + Show(AccountNamedOpt), + #[structopt(name = "generate_secret")] + Generate(AccountNamedOpt), + #[structopt(name = "delete_secret")] + Delete(AccountNamedOpt), +} + +#[derive(Debug, StructOpt)] +pub struct AccountPosixOpt { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt(long = "gidnumber")] + gidnumber: Option, + #[structopt(long = "shell")] + shell: Option, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub enum AccountPosix { + #[structopt(name = "show")] + Show(AccountNamedOpt), + #[structopt(name = "set")] + Set(AccountPosixOpt), + #[structopt(name = "set_password")] + SetPassword(AccountNamedOpt), +} + +#[derive(Debug, StructOpt)] +pub enum AccountSsh { + #[structopt(name = "list_publickeys")] + List(AccountNamedOpt), + #[structopt(name = "add_publickey")] + Add(AccountNamedTagPKOpt), + #[structopt(name = "delete_publickey")] + Delete(AccountNamedTagOpt), +} + +#[derive(Debug, StructOpt)] +pub enum AccountOpt { + #[structopt(name = "credential")] + Credential(AccountCredential), + #[structopt(name = "radius")] + Radius(AccountRadius), + #[structopt(name = "posix")] + Posix(AccountPosix), + #[structopt(name = "ssh")] + Ssh(AccountSsh), + #[structopt(name = "list")] + List(CommonOpt), + #[structopt(name = "get")] + Get(AccountNamedOpt), + #[structopt(name = "create")] + Create(AccountCreateOpt), + #[structopt(name = "delete")] + Delete(AccountNamedOpt), +} + +impl AccountOpt { + pub fn debug(&self) -> bool { + match self { + AccountOpt::Credential(acopt) => match acopt { + AccountCredential::SetPassword(acs) => acs.copt.debug, + AccountCredential::GeneratePassword(acs) => acs.copt.debug, + }, + AccountOpt::Radius(acopt) => match acopt { + AccountRadius::Show(aro) => aro.copt.debug, + AccountRadius::Generate(aro) => aro.copt.debug, + AccountRadius::Delete(aro) => aro.copt.debug, + }, + AccountOpt::Posix(apopt) => match apopt { + AccountPosix::Show(apo) => apo.copt.debug, + AccountPosix::Set(apo) => apo.copt.debug, + AccountPosix::SetPassword(apo) => apo.copt.debug, + }, + AccountOpt::Ssh(asopt) => match asopt { + AccountSsh::List(ano) => ano.copt.debug, + AccountSsh::Add(ano) => ano.copt.debug, + AccountSsh::Delete(ano) => ano.copt.debug, + }, + AccountOpt::List(copt) => copt.debug, + AccountOpt::Get(aopt) => aopt.copt.debug, + AccountOpt::Delete(aopt) => aopt.copt.debug, + AccountOpt::Create(aopt) => aopt.copt.debug, + } + } + + pub fn exec(&self) -> () { + match self { + // 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 + AccountOpt::Radius(aropt) => match aropt { + AccountRadius::Show(aopt) => { + let client = aopt.copt.to_client(); + + let rcred = client + .idm_account_radius_credential_get(aopt.aopts.account_id.as_str()) + .unwrap(); + + match rcred { + Some(s) => println!("Radius secret: {}", s), + None => println!("NO Radius secret"), + } + } + AccountRadius::Generate(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str()) + .unwrap(); + } + AccountRadius::Delete(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_radius_credential_delete(aopt.aopts.account_id.as_str()) + .unwrap(); + } + }, // end AccountOpt::Radius + AccountOpt::Posix(apopt) => match apopt { + AccountPosix::Show(aopt) => { + let client = aopt.copt.to_client(); + let token = client + .idm_account_unix_token_get(aopt.aopts.account_id.as_str()) + .unwrap(); + println!("{:?}", token); + } + AccountPosix::Set(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_unix_extend( + aopt.aopts.account_id.as_str(), + aopt.gidnumber, + aopt.shell.as_deref(), + ) + .unwrap(); + } + AccountPosix::SetPassword(aopt) => { + let client = aopt.copt.to_client(); + let password = + rpassword::prompt_password_stderr("Enter new unix (sudo) password: ") + .unwrap(); + client + .idm_account_unix_cred_put( + aopt.aopts.account_id.as_str(), + password.as_str(), + ) + .unwrap(); + } + }, // end AccountOpt::Posix + AccountOpt::Ssh(asopt) => match asopt { + AccountSsh::List(aopt) => { + let client = aopt.copt.to_client(); + + let pkeys = client + .idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str()) + .unwrap(); + + for pkey in pkeys { + println!("{}", pkey) + } + } + AccountSsh::Add(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_post_ssh_pubkey( + aopt.aopts.account_id.as_str(), + aopt.tag.as_str(), + aopt.pubkey.as_str(), + ) + .unwrap(); + } + AccountSsh::Delete(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_delete_ssh_pubkey( + aopt.aopts.account_id.as_str(), + aopt.tag.as_str(), + ) + .unwrap(); + } + }, // end AccountOpt::Ssh + AccountOpt::List(copt) => { + let client = copt.to_client(); + let r = client.idm_account_list().unwrap(); + for e in r { + println!("{:?}", e); + } + } + AccountOpt::Get(aopt) => { + let client = aopt.copt.to_client(); + let e = client + .idm_account_get(aopt.aopts.account_id.as_str()) + .unwrap(); + println!("{:?}", e); + } + AccountOpt::Delete(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_delete(aopt.aopts.account_id.as_str()) + .unwrap(); + } + AccountOpt::Create(acopt) => { + let client = acopt.copt.to_client(); + client + .idm_account_create( + acopt.aopts.account_id.as_str(), + acopt.display_name.as_str(), + ) + .unwrap(); + } + } + } +} diff --git a/kanidm_tools/src/cli/common.rs b/kanidm_tools/src/cli/common.rs new file mode 100644 index 000000000..9df7d1112 --- /dev/null +++ b/kanidm_tools/src/cli/common.rs @@ -0,0 +1,70 @@ +use kanidm_client::{KanidmClient, KanidmClientBuilder}; +use shellexpand; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +pub struct Named { + #[structopt()] + pub name: String, + #[structopt(flatten)] + pub copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub struct CommonOpt { + #[structopt(short = "d", long = "debug")] + pub debug: bool, + #[structopt(short = "H", long = "url")] + pub addr: Option, + #[structopt(short = "D", long = "name")] + pub username: String, + #[structopt(parse(from_os_str), short = "C", long = "ca")] + pub ca_path: Option, +} + +impl CommonOpt { + pub fn to_client(&self) -> KanidmClient { + let config_path: String = shellexpand::tilde("~/.config/kanidm").into_owned(); + + debug!("Attempting to use config {}", "/etc/kanidm/config"); + let client_builder = KanidmClientBuilder::new() + .read_options_from_optional_config("/etc/kanidm/config") + .and_then(|cb| { + debug!("Attempting to use config {}", config_path); + cb.read_options_from_optional_config(config_path) + }) + .expect("Failed to parse config (if present)"); + + let client_builder = match &self.addr { + Some(a) => client_builder.address(a.to_string()), + None => client_builder, + }; + + let ca_path: Option<&str> = self.ca_path.as_ref().map(|p| p.to_str().unwrap()); + let client_builder = match ca_path { + Some(p) => client_builder + .add_root_certificate_filepath(p) + .expect("Failed to access CA file"), + None => client_builder, + }; + + let client = client_builder + .build() + .expect("Failed to build client instance"); + + let r = if self.username == "anonymous" { + client.auth_anonymous() + } else { + let password = rpassword::prompt_password_stderr("Enter password: ").unwrap(); + client.auth_simple_password(self.username.as_str(), password.as_str()) + }; + + if r.is_err() { + println!("Error during authentication phase: {:?}", r); + std::process::exit(1); + } + + client + } +} diff --git a/kanidm_tools/src/cli/group.rs b/kanidm_tools/src/cli/group.rs new file mode 100644 index 000000000..3d5186c01 --- /dev/null +++ b/kanidm_tools/src/cli/group.rs @@ -0,0 +1,132 @@ +use crate::common::{CommonOpt, Named}; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +pub struct GroupNamedMembers { + #[structopt()] + name: String, + #[structopt()] + members: Vec, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub struct GroupPosixOpt { + #[structopt()] + name: String, + #[structopt(long = "gidnumber")] + gidnumber: Option, + #[structopt(flatten)] + copt: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub enum GroupPosix { + #[structopt(name = "show")] + Show(Named), + #[structopt(name = "set")] + Set(GroupPosixOpt), +} + +#[derive(Debug, StructOpt)] +pub enum GroupOpt { + #[structopt(name = "list")] + List(CommonOpt), + #[structopt(name = "create")] + Create(Named), + #[structopt(name = "delete")] + Delete(Named), + #[structopt(name = "list_members")] + ListMembers(Named), + #[structopt(name = "set_members")] + SetMembers(GroupNamedMembers), + #[structopt(name = "purge_members")] + PurgeMembers(Named), + #[structopt(name = "add_members")] + AddMembers(GroupNamedMembers), + #[structopt(name = "posix")] + Posix(GroupPosix), +} + +impl GroupOpt { + pub fn debug(&self) -> bool { + match self { + GroupOpt::List(copt) => copt.debug, + GroupOpt::Create(gcopt) => gcopt.copt.debug, + GroupOpt::Delete(gcopt) => gcopt.copt.debug, + GroupOpt::ListMembers(gcopt) => gcopt.copt.debug, + GroupOpt::AddMembers(gcopt) => gcopt.copt.debug, + GroupOpt::SetMembers(gcopt) => gcopt.copt.debug, + GroupOpt::PurgeMembers(gcopt) => gcopt.copt.debug, + GroupOpt::Posix(gpopt) => match gpopt { + GroupPosix::Show(gcopt) => gcopt.copt.debug, + GroupPosix::Set(gcopt) => gcopt.copt.debug, + }, + } + } + + pub fn exec(&self) -> () { + match self { + GroupOpt::List(copt) => { + let client = copt.to_client(); + let r = client.idm_group_list().unwrap(); + for e in r { + println!("{:?}", e); + } + } + GroupOpt::Create(gcopt) => { + let client = gcopt.copt.to_client(); + client.idm_group_create(gcopt.name.as_str()).unwrap(); + } + GroupOpt::Delete(gcopt) => { + let client = gcopt.copt.to_client(); + client.idm_group_delete(gcopt.name.as_str()).unwrap(); + } + GroupOpt::PurgeMembers(gcopt) => { + let client = gcopt.copt.to_client(); + client.idm_group_purge_members(gcopt.name.as_str()).unwrap(); + } + GroupOpt::ListMembers(gcopt) => { + let client = gcopt.copt.to_client(); + let members = client.idm_group_get_members(gcopt.name.as_str()).unwrap(); + if let Some(groups) = members { + for m in groups { + println!("{:?}", m); + } + } + } + GroupOpt::AddMembers(gcopt) => { + let client = gcopt.copt.to_client(); + let new_members: Vec<&str> = gcopt.members.iter().map(|s| s.as_str()).collect(); + + client + .idm_group_add_members(gcopt.name.as_str(), new_members) + .unwrap(); + } + GroupOpt::SetMembers(gcopt) => { + let client = gcopt.copt.to_client(); + let new_members: Vec<&str> = gcopt.members.iter().map(|s| s.as_str()).collect(); + + client + .idm_group_set_members(gcopt.name.as_str(), new_members) + .unwrap(); + } + GroupOpt::Posix(gpopt) => match gpopt { + GroupPosix::Show(gcopt) => { + let client = gcopt.copt.to_client(); + let token = client + .idm_group_unix_token_get(gcopt.name.as_str()) + .unwrap(); + println!("{:?}", token); + } + GroupPosix::Set(gcopt) => { + let client = gcopt.copt.to_client(); + client + .idm_group_unix_extend(gcopt.name.as_str(), gcopt.gidnumber) + .unwrap(); + } + }, + } // end match + } +} diff --git a/kanidm_tools/src/cli/lib.rs b/kanidm_tools/src/cli/lib.rs new file mode 100644 index 000000000..6cc43f553 --- /dev/null +++ b/kanidm_tools/src/cli/lib.rs @@ -0,0 +1,103 @@ +#[macro_use] +extern crate log; +use structopt::StructOpt; + +pub mod account; +pub mod common; +pub mod group; +pub mod raw; +pub mod recycle; + +use crate::account::AccountOpt; +use crate::common::CommonOpt; +use crate::group::GroupOpt; +use crate::raw::RawOpt; +use crate::recycle::RecycleOpt; + +#[derive(Debug, StructOpt)] +pub enum SelfOpt { + #[structopt(name = "whoami")] + /// Show the current authenticated user's identity + Whoami(CommonOpt), + #[structopt(name = "set_password")] + /// Set the current user's password + SetPassword(CommonOpt), +} + +impl SelfOpt { + pub fn debug(&self) -> bool { + match self { + SelfOpt::Whoami(copt) => copt.debug, + SelfOpt::SetPassword(copt) => copt.debug, + } + } + + pub fn exec(&self) -> () { + match self { + 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(); + + client.idm_account_set_password(password).unwrap(); + } + } + } +} + +#[derive(Debug, StructOpt)] +#[structopt(about = "I am a program and I work, just pass `-h`")] +pub enum ClientOpt { + #[structopt(name = "self")] + /// Actions for the current authenticated account + CSelf(SelfOpt), + #[structopt(name = "account")] + /// Account operations + Account(AccountOpt), + #[structopt(name = "group")] + /// Group operations + Group(GroupOpt), + #[structopt(name = "recycle_bin")] + /// Recycle Bin operations + Recycle(RecycleOpt), + #[structopt(name = "raw")] + /// Unsafe - low level, raw database operations. + Raw(RawOpt), +} + +impl ClientOpt { + pub fn debug(&self) -> bool { + match self { + ClientOpt::Raw(ropt) => ropt.debug(), + ClientOpt::CSelf(csopt) => csopt.debug(), + ClientOpt::Account(aopt) => aopt.debug(), + ClientOpt::Group(gopt) => gopt.debug(), + ClientOpt::Recycle(ropt) => ropt.debug(), + } + } + + pub fn exec(&self) -> () { + match self { + ClientOpt::Raw(ropt) => ropt.exec(), + ClientOpt::CSelf(csopt) => csopt.exec(), + ClientOpt::Account(aopt) => aopt.exec(), + ClientOpt::Group(gopt) => gopt.exec(), + ClientOpt::Recycle(ropt) => ropt.exec(), + } + } +} diff --git a/kanidm_tools/src/cli/main.rs b/kanidm_tools/src/cli/main.rs new file mode 100644 index 000000000..67691199b --- /dev/null +++ b/kanidm_tools/src/cli/main.rs @@ -0,0 +1,15 @@ +use kanidm_cli::ClientOpt; +use structopt::StructOpt; + +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(); + + opt.exec() +} diff --git a/kanidm_tools/src/cli/raw.rs b/kanidm_tools/src/cli/raw.rs new file mode 100644 index 000000000..91ed3e680 --- /dev/null +++ b/kanidm_tools/src/cli/raw.rs @@ -0,0 +1,118 @@ +use crate::common::CommonOpt; +use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList}; +use std::collections::BTreeMap; +use structopt::StructOpt; + +use std::error::Error; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use std::path::PathBuf; + +use serde::de::DeserializeOwned; + +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) +} + +#[derive(Debug, StructOpt)] +pub struct FilterOpt { + #[structopt()] + filter: String, + #[structopt(flatten)] + commonopts: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub struct CreateOpt { + #[structopt(parse(from_os_str))] + file: Option, + #[structopt(flatten)] + commonopts: CommonOpt, +} + +#[derive(Debug, StructOpt)] +pub struct ModifyOpt { + #[structopt(flatten)] + commonopts: CommonOpt, + #[structopt()] + filter: String, + #[structopt(parse(from_os_str))] + file: Option, +} + +#[derive(Debug, StructOpt)] +pub enum RawOpt { + #[structopt(name = "search")] + Search(FilterOpt), + #[structopt(name = "create")] + Create(CreateOpt), + #[structopt(name = "modify")] + Modify(ModifyOpt), + #[structopt(name = "delete")] + Delete(FilterOpt), +} + +impl RawOpt { + pub fn debug(&self) -> bool { + match self { + RawOpt::Search(sopt) => sopt.commonopts.debug, + RawOpt::Create(copt) => copt.commonopts.debug, + RawOpt::Modify(mopt) => mopt.commonopts.debug, + RawOpt::Delete(dopt) => dopt.commonopts.debug, + } + } + + pub fn exec(&self) -> () { + match self { + RawOpt::Search(sopt) => { + let client = sopt.commonopts.to_client(); + + 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!("Must provide a file"); + } + } + } + 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"); + } + } + } + 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(); + } + } + } +} diff --git a/kanidm_tools/src/cli/recycle.rs b/kanidm_tools/src/cli/recycle.rs new file mode 100644 index 000000000..1a550b036 --- /dev/null +++ b/kanidm_tools/src/cli/recycle.rs @@ -0,0 +1,46 @@ +use crate::common::{CommonOpt, Named}; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +pub enum RecycleOpt { + #[structopt(name = "list")] + /// List objects that are in the recycle bin + List(CommonOpt), + #[structopt(name = "get")] + /// Display an object from the recycle bin + Get(Named), + #[structopt(name = "revive")] + /// Revive a recycled object into a live (accessible) state - this is the opposite of "delete" + Revive(Named), +} + +impl RecycleOpt { + pub fn debug(&self) -> bool { + match self { + RecycleOpt::List(copt) => copt.debug, + RecycleOpt::Get(nopt) => nopt.copt.debug, + RecycleOpt::Revive(nopt) => nopt.copt.debug, + } + } + + pub fn exec(&self) -> () { + match self { + RecycleOpt::List(copt) => { + let client = copt.to_client(); + let r = client.recycle_bin_list().unwrap(); + for e in r { + println!("{:?}", e); + } + } + RecycleOpt::Get(nopt) => { + let client = nopt.copt.to_client(); + let e = client.recycle_bin_get(nopt.name.as_str()).unwrap(); + println!("{:?}", e); + } + RecycleOpt::Revive(nopt) => { + let client = nopt.copt.to_client(); + client.recycle_bin_revive(nopt.name.as_str()).unwrap(); + } + } + } +} diff --git a/kanidm_tools/src/main.rs b/kanidm_tools/src/main.rs deleted file mode 100644 index 07ed6165a..000000000 --- a/kanidm_tools/src/main.rs +++ /dev/null @@ -1,670 +0,0 @@ -use std::collections::BTreeMap; -use std::error::Error; -use std::fs::File; -use std::io::BufReader; -use std::path::Path; -use std::path::PathBuf; - -use kanidm_client::{KanidmClient, KanidmClientBuilder}; -use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList}; - -use log::debug; -use serde::de::DeserializeOwned; -use shellexpand; -use structopt::StructOpt; - -#[derive(Debug, StructOpt)] -struct CommonOpt { - #[structopt(short = "d", long = "debug")] - debug: bool, - #[structopt(short = "H", long = "url")] - addr: Option, - #[structopt(short = "D", long = "name")] - username: String, - #[structopt(parse(from_os_str), short = "C", long = "ca")] - ca_path: Option, -} - -impl CommonOpt { - fn to_client(&self) -> KanidmClient { - let config_path: String = shellexpand::tilde("~/.config/kanidm").into_owned(); - - debug!("Attempting to use config {}", "/etc/kanidm/config"); - let client_builder = KanidmClientBuilder::new() - .read_options_from_optional_config("/etc/kanidm/config") - .and_then(|cb| { - debug!("Attempting to use config {}", config_path); - cb.read_options_from_optional_config(config_path) - }) - .expect("Failed to parse config (if present)"); - - let client_builder = match &self.addr { - Some(a) => client_builder.address(a.to_string()), - None => client_builder, - }; - - let ca_path: Option<&str> = self.ca_path.as_ref().map(|p| p.to_str().unwrap()); - let client_builder = match ca_path { - Some(p) => client_builder - .add_root_certificate_filepath(p) - .expect("Failed to access CA file"), - None => client_builder, - }; - - let client = client_builder - .build() - .expect("Failed to build client instance"); - - let r = if self.username == "anonymous" { - client.auth_anonymous() - } else { - let password = rpassword::prompt_password_stderr("Enter password: ").unwrap(); - client.auth_simple_password(self.username.as_str(), password.as_str()) - }; - - if r.is_err() { - println!("Error during authentication phase: {:?}", r); - std::process::exit(1); - } - - client - } -} - -#[derive(Debug, StructOpt)] -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)] -struct AccountNamedOpt { - #[structopt(flatten)] - aopts: AccountCommonOpt, - #[structopt(flatten)] - copt: CommonOpt, -} - -#[derive(Debug, StructOpt)] -struct AccountNamedTagOpt { - #[structopt(flatten)] - aopts: AccountCommonOpt, - #[structopt(flatten)] - copt: CommonOpt, - #[structopt(name = "tag")] - tag: String, -} - -#[derive(Debug, StructOpt)] -struct AccountNamedTagPKOpt { - #[structopt(flatten)] - aopts: AccountCommonOpt, - #[structopt(flatten)] - copt: CommonOpt, - #[structopt(name = "tag")] - tag: String, - #[structopt(name = "pubkey")] - pubkey: String, -} - -#[derive(Debug, StructOpt)] -struct AccountCreateOpt { - #[structopt(flatten)] - aopts: AccountCommonOpt, - #[structopt(name = "display_name")] - display_name: String, - #[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 AccountRadius { - #[structopt(name = "show_secret")] - Show(AccountNamedOpt), - #[structopt(name = "generate_secret")] - Generate(AccountNamedOpt), - #[structopt(name = "delete_secret")] - Delete(AccountNamedOpt), -} - -#[derive(Debug, StructOpt)] -struct AccountPosixOpt { - #[structopt(flatten)] - aopts: AccountCommonOpt, - #[structopt(long = "gidnumber")] - gidnumber: Option, - #[structopt(long = "shell")] - shell: Option, - #[structopt(flatten)] - copt: CommonOpt, -} - -#[derive(Debug, StructOpt)] -enum AccountPosix { - #[structopt(name = "show")] - Show(AccountNamedOpt), - #[structopt(name = "set")] - Set(AccountPosixOpt), - #[structopt(name = "set_password")] - SetPassword(AccountNamedOpt), -} - -#[derive(Debug, StructOpt)] -enum AccountSsh { - #[structopt(name = "list_publickeys")] - List(AccountNamedOpt), - #[structopt(name = "add_publickey")] - Add(AccountNamedTagPKOpt), - #[structopt(name = "delete_publickey")] - Delete(AccountNamedTagOpt), -} - -#[derive(Debug, StructOpt)] -enum AccountOpt { - #[structopt(name = "credential")] - Credential(AccountCredential), - #[structopt(name = "radius")] - Radius(AccountRadius), - #[structopt(name = "posix")] - Posix(AccountPosix), - #[structopt(name = "ssh")] - Ssh(AccountSsh), - #[structopt(name = "list")] - List(CommonOpt), - #[structopt(name = "get")] - Get(AccountNamedOpt), - #[structopt(name = "create")] - Create(AccountCreateOpt), - #[structopt(name = "delete")] - Delete(AccountNamedOpt), -} - -#[derive(Debug, StructOpt)] -struct GroupNamed { - #[structopt()] - name: String, - #[structopt(flatten)] - copt: CommonOpt, -} - -#[derive(Debug, StructOpt)] -struct GroupNamedMembers { - #[structopt()] - name: String, - #[structopt()] - members: Vec, - #[structopt(flatten)] - copt: CommonOpt, -} - -#[derive(Debug, StructOpt)] -struct GroupPosixOpt { - #[structopt()] - name: String, - #[structopt(long = "gidnumber")] - gidnumber: Option, - #[structopt(flatten)] - copt: CommonOpt, -} - -#[derive(Debug, StructOpt)] -enum GroupPosix { - #[structopt(name = "show")] - Show(GroupNamed), - #[structopt(name = "set")] - Set(GroupPosixOpt), -} - -#[derive(Debug, StructOpt)] -enum GroupOpt { - #[structopt(name = "list")] - List(CommonOpt), - #[structopt(name = "create")] - Create(GroupNamed), - #[structopt(name = "delete")] - Delete(GroupNamed), - #[structopt(name = "list_members")] - ListMembers(GroupNamed), - #[structopt(name = "set_members")] - SetMembers(GroupNamedMembers), - #[structopt(name = "purge_members")] - PurgeMembers(GroupNamed), - #[structopt(name = "add_members")] - AddMembers(GroupNamedMembers), - #[structopt(name = "posix")] - Posix(GroupPosix), -} - -#[derive(Debug, StructOpt)] -enum SelfOpt { - #[structopt(name = "whoami")] - Whoami(CommonOpt), - #[structopt(name = "set_password")] - SetPassword(CommonOpt), -} - -#[derive(Debug, StructOpt)] -enum ClientOpt { - #[structopt(name = "raw")] - Raw(RawOpt), - #[structopt(name = "self")] - CSelf(SelfOpt), - #[structopt(name = "account")] - Account(AccountOpt), - #[structopt(name = "group")] - Group(GroupOpt), -} - -impl ClientOpt { - fn debug(&self) -> bool { - match self { - 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::Credential(acopt) => match acopt { - AccountCredential::SetPassword(acs) => acs.copt.debug, - AccountCredential::GeneratePassword(acs) => acs.copt.debug, - }, - AccountOpt::Radius(acopt) => match acopt { - AccountRadius::Show(aro) => aro.copt.debug, - AccountRadius::Generate(aro) => aro.copt.debug, - AccountRadius::Delete(aro) => aro.copt.debug, - }, - AccountOpt::Posix(apopt) => match apopt { - AccountPosix::Show(apo) => apo.copt.debug, - AccountPosix::Set(apo) => apo.copt.debug, - AccountPosix::SetPassword(apo) => apo.copt.debug, - }, - AccountOpt::Ssh(asopt) => match asopt { - AccountSsh::List(ano) => ano.copt.debug, - AccountSsh::Add(ano) => ano.copt.debug, - AccountSsh::Delete(ano) => ano.copt.debug, - }, - AccountOpt::List(copt) => copt.debug, - AccountOpt::Get(aopt) => aopt.copt.debug, - AccountOpt::Delete(aopt) => aopt.copt.debug, - AccountOpt::Create(aopt) => aopt.copt.debug, - }, - ClientOpt::Group(gopt) => match gopt { - GroupOpt::List(copt) => copt.debug, - GroupOpt::Create(gcopt) => gcopt.copt.debug, - GroupOpt::Delete(gcopt) => gcopt.copt.debug, - GroupOpt::ListMembers(gcopt) => gcopt.copt.debug, - GroupOpt::AddMembers(gcopt) => gcopt.copt.debug, - GroupOpt::SetMembers(gcopt) => gcopt.copt.debug, - GroupOpt::PurgeMembers(gcopt) => gcopt.copt.debug, - GroupOpt::Posix(gpopt) => match gpopt { - GroupPosix::Show(gcopt) => gcopt.copt.debug, - GroupPosix::Set(gcopt) => gcopt.copt.debug, - }, - }, - } - } -} - -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(); - - 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(); - - match opt { - ClientOpt::Raw(ropt) => match ropt { - RawOpt::Search(sopt) => { - let client = sopt.commonopts.to_client(); - - 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!("Must provide a file"); - } - } - } - 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"); - } - } - } - 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(); - - 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 - AccountOpt::Radius(aropt) => match aropt { - AccountRadius::Show(aopt) => { - let client = aopt.copt.to_client(); - - let rcred = client - .idm_account_radius_credential_get(aopt.aopts.account_id.as_str()) - .unwrap(); - - match rcred { - Some(s) => println!("Radius secret: {}", s), - None => println!("NO Radius secret"), - } - } - AccountRadius::Generate(aopt) => { - let client = aopt.copt.to_client(); - client - .idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str()) - .unwrap(); - } - AccountRadius::Delete(aopt) => { - let client = aopt.copt.to_client(); - client - .idm_account_radius_credential_delete(aopt.aopts.account_id.as_str()) - .unwrap(); - } - }, // end AccountOpt::Radius - AccountOpt::Posix(apopt) => match apopt { - AccountPosix::Show(aopt) => { - let client = aopt.copt.to_client(); - let token = client - .idm_account_unix_token_get(aopt.aopts.account_id.as_str()) - .unwrap(); - println!("{:?}", token); - } - AccountPosix::Set(aopt) => { - let client = aopt.copt.to_client(); - client - .idm_account_unix_extend( - aopt.aopts.account_id.as_str(), - aopt.gidnumber, - aopt.shell.as_deref(), - ) - .unwrap(); - } - AccountPosix::SetPassword(aopt) => { - let client = aopt.copt.to_client(); - let password = - rpassword::prompt_password_stderr("Enter new unix (sudo) password: ") - .unwrap(); - client - .idm_account_unix_cred_put( - aopt.aopts.account_id.as_str(), - password.as_str(), - ) - .unwrap(); - } - }, // end AccountOpt::Posix - AccountOpt::Ssh(asopt) => match asopt { - AccountSsh::List(aopt) => { - let client = aopt.copt.to_client(); - - let pkeys = client - .idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str()) - .unwrap(); - - for pkey in pkeys { - println!("{}", pkey) - } - } - AccountSsh::Add(aopt) => { - let client = aopt.copt.to_client(); - client - .idm_account_post_ssh_pubkey( - aopt.aopts.account_id.as_str(), - aopt.tag.as_str(), - aopt.pubkey.as_str(), - ) - .unwrap(); - } - AccountSsh::Delete(aopt) => { - let client = aopt.copt.to_client(); - client - .idm_account_delete_ssh_pubkey( - aopt.aopts.account_id.as_str(), - aopt.tag.as_str(), - ) - .unwrap(); - } - }, // end AccountOpt::Ssh - AccountOpt::List(copt) => { - let client = copt.to_client(); - let r = client.idm_account_list().unwrap(); - for e in r { - println!("{:?}", e); - } - } - AccountOpt::Get(aopt) => { - let client = aopt.copt.to_client(); - let e = client - .idm_account_get(aopt.aopts.account_id.as_str()) - .unwrap(); - println!("{:?}", e); - } - AccountOpt::Delete(aopt) => { - let client = aopt.copt.to_client(); - client - .idm_account_delete(aopt.aopts.account_id.as_str()) - .unwrap(); - } - AccountOpt::Create(acopt) => { - let client = acopt.copt.to_client(); - client - .idm_account_create( - acopt.aopts.account_id.as_str(), - acopt.display_name.as_str(), - ) - .unwrap(); - } - }, // end Account - ClientOpt::Group(gopt) => match gopt { - GroupOpt::List(copt) => { - let client = copt.to_client(); - let r = client.idm_group_list().unwrap(); - for e in r { - println!("{:?}", e); - } - } - GroupOpt::Create(gcopt) => { - let client = gcopt.copt.to_client(); - client.idm_group_create(gcopt.name.as_str()).unwrap(); - } - GroupOpt::Delete(gcopt) => { - let client = gcopt.copt.to_client(); - client.idm_group_delete(gcopt.name.as_str()).unwrap(); - } - GroupOpt::PurgeMembers(gcopt) => { - let client = gcopt.copt.to_client(); - client.idm_group_purge_members(gcopt.name.as_str()).unwrap(); - } - GroupOpt::ListMembers(gcopt) => { - let client = gcopt.copt.to_client(); - let members = client.idm_group_get_members(gcopt.name.as_str()).unwrap(); - if let Some(groups) = members { - for m in groups { - println!("{:?}", m); - } - } - } - GroupOpt::AddMembers(gcopt) => { - let client = gcopt.copt.to_client(); - let new_members: Vec<&str> = gcopt.members.iter().map(|s| s.as_str()).collect(); - - client - .idm_group_add_members(gcopt.name.as_str(), new_members) - .unwrap(); - } - GroupOpt::SetMembers(gcopt) => { - let client = gcopt.copt.to_client(); - let new_members: Vec<&str> = gcopt.members.iter().map(|s| s.as_str()).collect(); - - client - .idm_group_set_members(gcopt.name.as_str(), new_members) - .unwrap(); - } - GroupOpt::Posix(gpopt) => match gpopt { - GroupPosix::Show(gcopt) => { - let client = gcopt.copt.to_client(); - let token = client - .idm_group_unix_token_get(gcopt.name.as_str()) - .unwrap(); - println!("{:?}", token); - } - GroupPosix::Set(gcopt) => { - let client = gcopt.copt.to_client(); - client - .idm_group_unix_extend(gcopt.name.as_str(), gcopt.gidnumber) - .unwrap(); - } - }, - }, // end Group - } -} diff --git a/kanidmd/src/lib/actors/v1_read.rs b/kanidmd/src/lib/actors/v1_read.rs index ea8a5a8b4..c7b4db005 100644 --- a/kanidmd/src/lib/actors/v1_read.rs +++ b/kanidmd/src/lib/actors/v1_read.rs @@ -86,6 +86,16 @@ impl Message for InternalSearchMessage { type Result = Result, OperationError>; } +pub struct InternalSearchRecycledMessage { + pub uat: Option, + pub filter: Filter, + pub attrs: Option>, +} + +impl Message for InternalSearchRecycledMessage { + type Result = Result, OperationError>; +} + pub struct InternalRadiusReadMessage { pub uat: Option, pub uuid_or_name: String, @@ -353,6 +363,40 @@ impl Handler for QueryServerReadV1 { } } +impl Handler for QueryServerReadV1 { + type Result = Result, OperationError>; + + fn handle( + &mut self, + msg: InternalSearchRecycledMessage, + _: &mut Self::Context, + ) -> Self::Result { + let mut audit = AuditScope::new("internal_search_recycle_message"); + let res = audit_segment!(&mut audit, || { + let qs_read = self.qs.read(); + + // Make an event from the request + let srch = match SearchEvent::from_internal_recycle_message(&mut audit, msg, &qs_read) { + Ok(s) => s, + Err(e) => { + audit_log!(audit, "Failed to begin recycled search: {:?}", e); + return Err(e); + } + }; + + audit_log!(audit, "Begin event {:?}", srch); + + match qs_read.search_ext(&mut audit, &srch) { + Ok(entries) => SearchResult::new(&mut audit, &qs_read, entries) + .map(|ok_sr| ok_sr.into_proto_array()), + Err(e) => Err(e), + } + }); + self.log.do_send(audit); + res + } +} + impl Handler for QueryServerReadV1 { type Result = Result, OperationError>; diff --git a/kanidmd/src/lib/actors/v1_write.rs b/kanidmd/src/lib/actors/v1_write.rs index 557665415..b9c079e7b 100644 --- a/kanidmd/src/lib/actors/v1_write.rs +++ b/kanidmd/src/lib/actors/v1_write.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use crate::async_log::EventLog; use crate::event::{ CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, + ReviveRecycledEvent, }; use crate::idm::event::{ GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent, @@ -90,6 +91,15 @@ impl Message for ModifyMessage { type Result = Result; } +pub struct ReviveRecycledMessage { + pub uat: Option, + pub filter: Filter, +} + +impl Message for ReviveRecycledMessage { + type Result = Result<(), OperationError>; +} + pub struct IdmAccountSetPasswordMessage { pub uat: Option, pub cleartext: String, @@ -485,6 +495,34 @@ impl Handler for QueryServerWriteV1 { } } +impl Handler for QueryServerWriteV1 { + type Result = Result<(), OperationError>; + + fn handle(&mut self, msg: ReviveRecycledMessage, _: &mut Self::Context) -> Self::Result { + let mut audit = AuditScope::new("revive"); + let res = audit_segment!(&mut audit, || { + let mut qs_write = self.qs.write(duration_from_epoch_now()); + + let rev = + match ReviveRecycledEvent::from_parts(&mut audit, msg.uat, msg.filter, &qs_write) { + Ok(r) => r, + Err(e) => { + audit_log!(audit, "Failed to begin revive: {:?}", e); + return Err(e); + } + }; + + audit_log!(audit, "Begin revive event {:?}", rev); + + qs_write + .revive_recycled(&mut audit, &rev) + .and_then(|_| qs_write.commit(&mut audit).map(|_| ())) + }); + self.log.do_send(audit); + res + } +} + // IDM native types for modifications impl Handler for QueryServerWriteV1 { type Result = Result, OperationError>; diff --git a/kanidmd/src/lib/constants/mod.rs b/kanidmd/src/lib/constants/mod.rs index 409762716..c0bd533d8 100644 --- a/kanidmd/src/lib/constants/mod.rs +++ b/kanidmd/src/lib/constants/mod.rs @@ -8,10 +8,25 @@ pub use crate::constants::system_config::JSON_SYSTEM_CONFIG_V1; pub static SYSTEM_INDEX_VERSION: i64 = 5; // On test builds, define to 60 seconds #[cfg(test)] -pub static PURGE_TIMEOUT: u64 = 60; -// For production, 1 hour. +pub static PURGE_FREQUENCY: u64 = 60; +// For production, 10 minutes. #[cfg(not(test))] -pub static PURGE_TIMEOUT: u64 = 3600; +pub static PURGE_FREQUENCY: u64 = 600; + +#[cfg(test)] +/// In test, we limit the changelog to 10 minutes. +pub static CHANGELOG_MAX_AGE: u64 = 600; +#[cfg(not(test))] +/// A replica may be less than 1 day out of sync and catch up. +pub static CHANGELOG_MAX_AGE: u64 = 86400; + +#[cfg(test)] +/// In test, we limit the recyclebin to 5 minutes. +pub static RECYCLEBIN_MAX_AGE: u64 = 300; +#[cfg(not(test))] +/// In production we allow 1 week +pub static RECYCLEBIN_MAX_AGE: u64 = 604800; + // 5 minute auth session window. pub static AUTH_SESSION_TIMEOUT: u64 = 300; pub static PW_MIN_LENGTH: usize = 10; diff --git a/kanidmd/src/lib/core/mod.rs b/kanidmd/src/lib/core/mod.rs index 32620b8d3..dd36e8465 100644 --- a/kanidmd/src/lib/core/mod.rs +++ b/kanidmd/src/lib/core/mod.rs @@ -13,8 +13,8 @@ use crate::config::Configuration; use crate::actors::v1_read::QueryServerReadV1; use crate::actors::v1_read::{ AuthMessage, IdmAccountUnixAuthMessage, InternalRadiusReadMessage, - InternalRadiusTokenReadMessage, InternalSearchMessage, InternalSshKeyReadMessage, - InternalSshKeyTagReadMessage, InternalUnixGroupTokenReadMessage, + InternalRadiusTokenReadMessage, InternalSearchMessage, InternalSearchRecycledMessage, + InternalSshKeyReadMessage, InternalSshKeyTagReadMessage, InternalUnixGroupTokenReadMessage, InternalUnixUserTokenReadMessage, SearchMessage, WhoamiMessage, }; use crate::actors::v1_write::QueryServerWriteV1; @@ -23,7 +23,7 @@ use crate::actors::v1_write::{ IdmAccountUnixExtendMessage, IdmAccountUnixSetCredMessage, IdmGroupUnixExtendMessage, InternalCredentialSetMessage, InternalDeleteMessage, InternalRegenerateRadiusMessage, InternalSshKeyCreateMessage, ModifyMessage, PurgeAttributeMessage, RemoveAttributeValueMessage, - SetAttributeMessage, + ReviveRecycledMessage, SetAttributeMessage, }; use crate::async_log; use crate::audit::AuditScope; @@ -866,6 +866,50 @@ async fn domain_id_put_attr( json_rest_event_put_id_attr(path, session, state, filter, values.into_inner()).await } +async fn recycle_bin_get((session, state): (Session, Data)) -> HttpResponse { + let filter = filter_all!(f_pres("class")); + let uat = get_current_user(&session); + let attrs = None; + + let obj = InternalSearchRecycledMessage { uat, filter, attrs }; + + match state.qe_r.send(obj).await { + Ok(Ok(r)) => HttpResponse::Ok().json(r), + Ok(Err(e)) => operation_error_to_response(e), + Err(_) => HttpResponse::InternalServerError().json("mailbox failure"), + } +} + +async fn recycle_bin_id_get( + (path, session, state): (Path, Session, Data), +) -> HttpResponse { + let uat = get_current_user(&session); + let filter = filter_all!(f_id(path.as_str())); + let attrs = None; + + let obj = InternalSearchRecycledMessage { uat, filter, attrs }; + + match state.qe_r.send(obj).await { + Ok(Ok(mut r)) => HttpResponse::Ok().json(r.pop()), + Ok(Err(e)) => operation_error_to_response(e), + Err(_) => HttpResponse::InternalServerError().json("mailbox failure"), + } +} + +async fn recycle_bin_revive_id_post( + (path, session, state): (Path, Session, Data), +) -> HttpResponse { + let uat = get_current_user(&session); + let filter = filter_all!(f_id(path.as_str())); + + let m_obj = ReviveRecycledMessage { uat, filter }; + match state.qe_w.send(m_obj).await { + Ok(Ok(r)) => HttpResponse::Ok().json(r), + Ok(Err(e)) => operation_error_to_response(e), + Err(_) => HttpResponse::InternalServerError().json("mailbox failure"), + } +} + async fn do_nothing(_session: Session) -> String { "did nothing".to_string() } @@ -1535,9 +1579,9 @@ pub fn create_server_core(config: Configuration) { ) .service( web::scope("/v1/recycle_bin") - .route("", web::get().to(do_nothing)) - .route("/{id}", web::get().to(do_nothing)) - .route("/{id}/_restore", web::get().to(do_nothing)), + .route("", web::get().to(recycle_bin_get)) + .route("/{id}", web::get().to(recycle_bin_id_get)) + .route("/{id}/_revive", web::post().to(recycle_bin_revive_id_post)), ) .service( web::scope("/v1/access_profile") diff --git a/kanidmd/src/lib/event.rs b/kanidmd/src/lib/event.rs index 9bbb3c5b9..ed396a8a4 100644 --- a/kanidmd/src/lib/event.rs +++ b/kanidmd/src/lib/event.rs @@ -16,7 +16,9 @@ use crate::server::{ }; use kanidm_proto::v1::OperationError; -use crate::actors::v1_read::{AuthMessage, InternalSearchMessage, SearchMessage}; +use crate::actors::v1_read::{ + AuthMessage, InternalSearchMessage, InternalSearchRecycledMessage, SearchMessage, +}; use crate::actors::v1_write::{CreateMessage, DeleteMessage, ModifyMessage}; // Bring in schematransaction trait for validate // use crate::schema::SchemaTransaction; @@ -280,6 +282,40 @@ impl SearchEvent { }) } + pub fn from_internal_recycle_message( + audit: &mut AuditScope, + msg: InternalSearchRecycledMessage, + qs: &QueryServerReadTransaction, + ) -> Result { + let r_attrs: Option> = msg.attrs.map(|vs| { + vs.into_iter() + .filter_map(|a| qs.get_schema().normalise_attr_if_exists(a.as_str())) + .collect() + }); + + if let Some(s) = &r_attrs { + if s.is_empty() { + return Err(OperationError::EmptyRequest); + } + } + + Ok(SearchEvent { + event: Event::from_ro_uat(audit, qs, msg.uat)?, + filter: msg + .filter + .clone() + .into_recycled() + .validate(qs.get_schema()) + .map_err(OperationError::SchemaViolation)?, + filter_orig: msg + .filter + .into_recycled() + .validate(qs.get_schema()) + .map_err(OperationError::SchemaViolation)?, + attrs: r_attrs, + }) + } + pub fn from_whoami_request( audit: &mut AuditScope, uat: Option, @@ -353,31 +389,6 @@ impl SearchEvent { } } - /* - #[cfg(test)] - #[allow(dead_code)] - pub fn from_rec_request( - audit: &mut AuditScope, - request: SearchRecycledRequest, - qs: &QueryServerReadTransaction, - ) -> Result { - match Filter::from_ro(audit, &request.filter, qs) { - Ok(f) => Ok(SearchEvent { - event: Event::from_ro_uat(audit, qs, msg.uat)?, - filter: f - .clone() - .into_recycled() - .validate(qs.get_schema()) - .map_err(|e| OperationError::SchemaViolation(e))?, - filter_orig: f - .validate(qs.get_schema()) - .map_err(|e| OperationError::SchemaViolation(e))?, - }), - Err(e) => Err(e), - } - } - */ - #[cfg(test)] /* Impersonate a request for recycled objects */ pub unsafe fn new_rec_impersonate_entry( @@ -1007,24 +1018,20 @@ impl Message for ReviveRecycledEvent { } impl ReviveRecycledEvent { - /* - pub fn from_message( + pub fn from_parts( audit: &mut AuditScope, - msg: ReviveRecycledMessage, + uat: Option, + filter: Filter, qs: &QueryServerWriteTransaction, ) -> Result { - match Filter::from_rw(audit, &msg.req.filter, qs) { - Ok(f) => Ok(ReviveRecycledEvent { - event: Event::from_rw_uat(audit, qs, msg.uat)?, - filter: f - .into_recycled() - .validate(qs.get_schema()) - .map_err(|e| OperationError::SchemaViolation(e))?, - }), - Err(e) => Err(e), - } + Ok(ReviveRecycledEvent { + event: Event::from_rw_uat(audit, qs, uat)?, + filter: filter + .into_recycled() + .validate(qs.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?, + }) } - */ #[cfg(test)] pub unsafe fn new_impersonate_entry( diff --git a/kanidmd/src/lib/interval.rs b/kanidmd/src/lib/interval.rs index 9c58afc1f..ef29d7048 100644 --- a/kanidmd/src/lib/interval.rs +++ b/kanidmd/src/lib/interval.rs @@ -2,7 +2,7 @@ use actix::prelude::*; use std::time::Duration; use crate::actors::v1_write::QueryServerWriteV1; -use crate::constants::PURGE_TIMEOUT; +use crate::constants::PURGE_FREQUENCY; use crate::event::{PurgeRecycledEvent, PurgeTombstoneEvent}; pub struct IntervalActor { @@ -33,10 +33,10 @@ impl Actor for IntervalActor { fn started(&mut self, ctx: &mut Self::Context) { // TODO #65: This timeout could be configurable from config? - ctx.run_interval(Duration::from_secs(PURGE_TIMEOUT), move |act, _ctx| { + ctx.run_interval(Duration::from_secs(PURGE_FREQUENCY), move |act, _ctx| { act.purge_recycled(); }); - ctx.run_interval(Duration::from_secs(PURGE_TIMEOUT), move |act, _ctx| { + ctx.run_interval(Duration::from_secs(PURGE_FREQUENCY), move |act, _ctx| { act.purge_tombstones(); }); } diff --git a/kanidmd/src/lib/repl/cid.rs b/kanidmd/src/lib/repl/cid.rs index c0f7c7567..8b8d5f9ae 100644 --- a/kanidmd/src/lib/repl/cid.rs +++ b/kanidmd/src/lib/repl/cid.rs @@ -1,3 +1,4 @@ +use kanidm_proto::v1::OperationError; use std::time::Duration; use uuid::Uuid; @@ -22,6 +23,17 @@ impl Cid { ts: Duration::new(0, 0), } } + + pub fn sub_secs(&self, secs: u64) -> Result { + self.ts + .checked_sub(Duration::from_secs(secs)) + .map(|r| Cid { + d_uuid: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(), + s_uuid: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(), + ts: r, + }) + .ok_or(OperationError::InvalidReplCID) + } } #[cfg(test)] diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index 6d97f9a0a..aac15228c 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -1138,20 +1138,25 @@ impl<'a> QueryServerWriteTransaction<'a> { pub fn purge_tombstones(&self, au: &mut AuditScope) -> Result<(), OperationError> { // delete everything that is a tombstone. + // TODO #68: Has an appropriate amount of time/condition past (ie replication events?) // Search for tombstones - let ts = - match self.internal_search(au, filter_all!(f_eq("class", PVCLASS_TOMBSTONE.clone()))) { - Ok(r) => r, - Err(e) => return Err(e), - }; + let cid = try_audit!(au, self.cid.sub_secs(CHANGELOG_MAX_AGE)); + let ts = match self.internal_search( + au, + filter_all!(f_and!([ + f_eq("class", PVCLASS_TOMBSTONE.clone()), + f_lt("last_modified_cid", PartialValue::new_cid(cid)), + ])), + ) { + Ok(r) => r, + Err(e) => return Err(e), + }; if ts.is_empty() { 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 let mut audit_be = AuditScope::new("backend_delete"); @@ -1175,11 +1180,18 @@ impl<'a> QueryServerWriteTransaction<'a> { pub fn purge_recycled(&self, au: &mut AuditScope) -> Result<(), OperationError> { // Send everything that is recycled to tombstone // Search all recycled - let rc = - match self.internal_search(au, filter_all!(f_eq("class", PVCLASS_RECYCLED.clone()))) { - Ok(r) => r, - Err(e) => return Err(e), - }; + + let cid = try_audit!(au, self.cid.sub_secs(RECYCLEBIN_MAX_AGE)); + let rc = match self.internal_search( + au, + filter_all!(f_and!([ + f_eq("class", PVCLASS_RECYCLED.clone()), + f_lt("last_modified_cid", PartialValue::new_cid(cid)), + ])), + ) { + Ok(r) => r, + Err(e) => return Err(e), + }; if rc.is_empty() { audit_log!(au, "No recycled present - purge operation success"); @@ -2084,7 +2096,7 @@ impl<'a> QueryServerWriteTransaction<'a> { #[cfg(test)] mod tests { use crate::audit::AuditScope; - use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN}; + use crate::constants::{CHANGELOG_MAX_AGE, JSON_ADMIN_V1, RECYCLEBIN_MAX_AGE, UUID_ADMIN}; use crate::credential::Credential; use crate::entry::{Entry, EntryInit, EntryNew}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent}; @@ -2092,6 +2104,7 @@ mod tests { use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::value::{PartialValue, Value}; use kanidm_proto::v1::{OperationError, SchemaError}; + use std::time::Duration; use uuid::Uuid; #[test] @@ -2459,7 +2472,11 @@ mod tests { #[test] fn test_qs_tombstone() { run_test!(|server: &QueryServer, audit: &mut AuditScope| { - let mut server_txn = server.write(duration_from_epoch_now()); + // First we setup some timestamps + let time_p1 = duration_from_epoch_now(); + let time_p2 = time_p1 + Duration::from_secs(CHANGELOG_MAX_AGE * 2); + + let mut server_txn = server.write(time_p1); let admin = server_txn .internal_search_uuid(audit, &UUID_ADMIN) .expect("failed"); @@ -2518,15 +2535,29 @@ mod tests { .expect("internal search failed"); assert!(r2.len() == 1); + // If we purge now, nothing happens, we aren't past the time window. + assert!(server_txn.purge_tombstones(audit).is_ok()); + + let r3 = server_txn + .internal_search(audit, filt_i_ts.clone()) + .expect("internal search failed"); + assert!(r3.len() == 1); + + // Commit + assert!(server_txn.commit(audit).is_ok()); + + // New txn, push the cid forward. + let server_txn = server.write(time_p2); + // Now purge assert!(server_txn.purge_tombstones(audit).is_ok()); // Assert it's gone // Internal search should not see it. - let r3 = server_txn + let r4 = server_txn .internal_search(audit, filt_i_ts) .expect("internal search failed"); - assert!(r3.is_empty()); + assert!(r4.is_empty()); assert!(server_txn.commit(audit).is_ok()); }) @@ -2535,7 +2566,11 @@ mod tests { #[test] fn test_qs_recycle_simple() { run_test!(|server: &QueryServer, audit: &mut AuditScope| { - let mut server_txn = server.write(duration_from_epoch_now()); + // First we setup some timestamps + let time_p1 = duration_from_epoch_now(); + let time_p2 = time_p1 + Duration::from_secs(RECYCLEBIN_MAX_AGE * 2); + + let mut server_txn = server.write(time_p1); let admin = server_txn .internal_search_uuid(audit, &UUID_ADMIN) .expect("failed"); @@ -2626,30 +2661,43 @@ mod tests { .expect("internal search failed"); assert!(r2.len() == 2); - // There are now two options - // revival + // There are now two paths forward + // revival or purge! assert!(server_txn.revive_recycled(audit, &rre_rc).is_ok()); - // purge to tombstone + // Not enough time has passed, won't have an effect for purge to TS assert!(server_txn.purge_recycled(audit).is_ok()); - - // Should be no recycled objects. let r3 = server_txn .internal_search(audit, filt_i_rc.clone()) .expect("internal search failed"); - assert!(r3.is_empty()); + assert!(r3.len() == 1); + + // Commit + assert!(server_txn.commit(audit).is_ok()); + + // Now, establish enough time for the recycled items to be purged. + let server_txn = server.write(time_p2); + + // purge to tombstone, now that time has passed. + assert!(server_txn.purge_recycled(audit).is_ok()); + + // Should be no recycled objects. + let r4 = server_txn + .internal_search(audit, filt_i_rc.clone()) + .expect("internal search failed"); + assert!(r4.is_empty()); // There should be one tombstone - let r4 = server_txn + let r5 = server_txn .internal_search(audit, filt_i_ts.clone()) .expect("internal search failed"); - assert!(r4.len() == 1); + assert!(r5.len() == 1); // There should be one entry - let r5 = server_txn + let r6 = server_txn .internal_search(audit, filt_i_per.clone()) .expect("internal search failed"); - assert!(r5.len() == 1); + assert!(r6.len() == 1); assert!(server_txn.commit(audit).is_ok()); })