mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
61c240e44b
commit
6388bcf6fc
|
@ -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)
|
||||
|
||||
|
|
40
kanidm_book/src/recycle_bin.md
Normal file
40
kanidm_book/src/recycle_bin.md
Normal 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>
|
||||
|
||||
|
||||
|
|
@ -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(), ())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -61,6 +61,7 @@ pub enum OperationError {
|
|||
InvalidState,
|
||||
InvalidEntryState,
|
||||
InvalidUuid,
|
||||
InvalidReplCID,
|
||||
InvalidACPState(String),
|
||||
InvalidSchemaState(String),
|
||||
InvalidAccountState(String),
|
||||
|
|
|
@ -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"
|
||||
|
|
311
kanidm_tools/src/cli/account.rs
Normal file
311
kanidm_tools/src/cli/account.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
kanidm_tools/src/cli/common.rs
Normal file
70
kanidm_tools/src/cli/common.rs
Normal 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
|
||||
}
|
||||
}
|
132
kanidm_tools/src/cli/group.rs
Normal file
132
kanidm_tools/src/cli/group.rs
Normal 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
103
kanidm_tools/src/cli/lib.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
15
kanidm_tools/src/cli/main.rs
Normal file
15
kanidm_tools/src/cli/main.rs
Normal 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
118
kanidm_tools/src/cli/raw.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
kanidm_tools/src/cli/recycle.rs
Normal file
46
kanidm_tools/src/cli/recycle.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
.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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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());
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue