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.
This commit is contained in:
Firstyear 2020-03-25 08:21:49 +10:00 committed by GitHub
parent 61c240e44b
commit 6388bcf6fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1183 additions and 751 deletions

View file

@ -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)

View file

@ -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 <id>
An entry can be revived with:
kanidm recycle_bin revive --name admin <id>

View file

@ -761,4 +761,17 @@ impl KanidmClient {
pub fn idm_schema_classtype_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
self.perform_get_request(format!("/v1/schema/classtype/{}", id).as_str())
}
// ==== recycle bin
pub fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/recycle_bin")
}
pub fn recycle_bin_get(&self, id: &str) -> Result<Option<Entry>, 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(), ())
}
}

View file

@ -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.

View file

@ -61,6 +61,7 @@ pub enum OperationError {
InvalidState,
InvalidEntryState,
InvalidUuid,
InvalidReplCID,
InvalidACPState(String),
InvalidSchemaState(String),
InvalidAccountState(String),

View file

@ -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"

View file

@ -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<String>,
#[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<u32>,
#[structopt(long = "shell")]
shell: Option<String>,
#[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();
}
}
}
}

View file

@ -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<String>,
#[structopt(short = "D", long = "name")]
pub username: String,
#[structopt(parse(from_os_str), short = "C", long = "ca")]
pub ca_path: Option<PathBuf>,
}
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
}
}

View file

@ -0,0 +1,132 @@
use crate::common::{CommonOpt, Named};
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub struct GroupNamedMembers {
#[structopt()]
name: String,
#[structopt()]
members: Vec<String>,
#[structopt(flatten)]
copt: CommonOpt,
}
#[derive(Debug, StructOpt)]
pub struct GroupPosixOpt {
#[structopt()]
name: String,
#[structopt(long = "gidnumber")]
gidnumber: Option<u32>,
#[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
}
}

103
kanidm_tools/src/cli/lib.rs Normal file
View file

@ -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(),
}
}
}

View file

@ -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()
}

118
kanidm_tools/src/cli/raw.rs Normal file
View file

@ -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<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T, Box<dyn Error>> {
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<PathBuf>,
#[structopt(flatten)]
commonopts: CommonOpt,
}
#[derive(Debug, StructOpt)]
pub struct ModifyOpt {
#[structopt(flatten)]
commonopts: CommonOpt,
#[structopt()]
filter: String,
#[structopt(parse(from_os_str))]
file: Option<PathBuf>,
}
#[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<BTreeMap<String, Vec<String>>> = 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<Modify> = 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();
}
}
}
}

View file

@ -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();
}
}
}
}

View file

@ -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<String>,
#[structopt(short = "D", long = "name")]
username: String,
#[structopt(parse(from_os_str), short = "C", long = "ca")]
ca_path: Option<PathBuf>,
}
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<PathBuf>,
#[structopt(flatten)]
commonopts: CommonOpt,
}
#[derive(Debug, StructOpt)]
struct ModifyOpt {
#[structopt(flatten)]
commonopts: CommonOpt,
#[structopt()]
filter: String,
#[structopt(parse(from_os_str))]
file: Option<PathBuf>,
}
#[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<String>,
#[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<u32>,
#[structopt(long = "shell")]
shell: Option<String>,
#[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<String>,
#[structopt(flatten)]
copt: CommonOpt,
}
#[derive(Debug, StructOpt)]
struct GroupPosixOpt {
#[structopt()]
name: String,
#[structopt(long = "gidnumber")]
gidnumber: Option<u32>,
#[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<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T, Box<dyn Error>> {
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<BTreeMap<String, Vec<String>>> = 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<Modify> = 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
}
}

View file

@ -86,6 +86,16 @@ impl Message for InternalSearchMessage {
type Result = Result<Vec<ProtoEntry>, OperationError>;
}
pub struct InternalSearchRecycledMessage {
pub uat: Option<UserAuthToken>,
pub filter: Filter<FilterInvalid>,
pub attrs: Option<Vec<String>>,
}
impl Message for InternalSearchRecycledMessage {
type Result = Result<Vec<ProtoEntry>, OperationError>;
}
pub struct InternalRadiusReadMessage {
pub uat: Option<UserAuthToken>,
pub uuid_or_name: String,
@ -353,6 +363,40 @@ impl Handler<InternalSearchMessage> for QueryServerReadV1 {
}
}
impl Handler<InternalSearchRecycledMessage> for QueryServerReadV1 {
type Result = Result<Vec<ProtoEntry>, 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<InternalRadiusReadMessage> for QueryServerReadV1 {
type Result = Result<Option<String>, OperationError>;

View file

@ -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<OperationResponse, OperationError>;
}
pub struct ReviveRecycledMessage {
pub uat: Option<UserAuthToken>,
pub filter: Filter<FilterInvalid>,
}
impl Message for ReviveRecycledMessage {
type Result = Result<(), OperationError>;
}
pub struct IdmAccountSetPasswordMessage {
pub uat: Option<UserAuthToken>,
pub cleartext: String,
@ -485,6 +495,34 @@ impl Handler<InternalDeleteMessage> for QueryServerWriteV1 {
}
}
impl Handler<ReviveRecycledMessage> 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<InternalCredentialSetMessage> for QueryServerWriteV1 {
type Result = Result<Option<String>, OperationError>;

View file

@ -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;

View file

@ -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<AppState>)) -> 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<String>, Session, Data<AppState>),
) -> 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<String>, Session, Data<AppState>),
) -> 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")

View file

@ -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<Self, OperationError> {
let r_attrs: Option<BTreeSet<String>> = 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<UserAuthToken>,
@ -353,31 +389,6 @@ impl SearchEvent {
}
}
/*
#[cfg(test)]
#[allow(dead_code)]
pub fn from_rec_request(
audit: &mut AuditScope,
request: SearchRecycledRequest,
qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> {
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<UserAuthToken>,
filter: Filter<FilterInvalid>,
qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> {
match Filter::from_rw(audit, &msg.req.filter, qs) {
Ok(f) => Ok(ReviveRecycledEvent {
event: Event::from_rw_uat(audit, qs, msg.uat)?,
filter: f
Ok(ReviveRecycledEvent {
event: Event::from_rw_uat(audit, qs, uat)?,
filter: filter
.into_recycled()
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
}),
Err(e) => Err(e),
})
}
}
*/
#[cfg(test)]
pub unsafe fn new_impersonate_entry(

View file

@ -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();
});
}

View file

@ -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, OperationError> {
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)]

View file

@ -1138,9 +1138,16 @@ 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()))) {
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),
};
@ -1150,8 +1157,6 @@ impl<'a> QueryServerWriteTransaction<'a> {
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,8 +1180,15 @@ 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()))) {
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),
};
@ -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());
})