[Fix] User deletion & migration
This commit is contained in:
parent
326ebf3b37
commit
7dc98ef29b
20 changed files with 742 additions and 129 deletions
|
|
@ -11,6 +11,7 @@ iota-core = { path = "../iota-core" }
|
|||
iota-process-manager = { path = "../iota-process-manager" }
|
||||
iota-paths = { path = "../iota-paths" }
|
||||
iota-terms = { path = "../iota-terms" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum, error::ErrorKind};
|
||||
use iota_cli::theme::{CliOutputFormat, ThemeName, UiConfig};
|
||||
use iota_terms::TermsType;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CliInvocation {
|
||||
|
|
@ -115,15 +116,33 @@ enum UsersAction {
|
|||
user_id: i64,
|
||||
},
|
||||
Add {
|
||||
username: String,
|
||||
username: Option<String>,
|
||||
#[arg(long, value_name = "PATH")]
|
||||
tu: Option<PathBuf>,
|
||||
},
|
||||
Remove {
|
||||
Release {
|
||||
user_id: i64,
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
Import {
|
||||
username: String,
|
||||
Data {
|
||||
#[command(subcommand)]
|
||||
action: UserDataAction,
|
||||
},
|
||||
CompleteDelete {
|
||||
user_id: i64,
|
||||
#[arg(long, value_name = "PATH")]
|
||||
tu: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum UserDataAction {
|
||||
Purge {
|
||||
user_id: i64,
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
|
|
@ -264,14 +283,21 @@ pub enum Command {
|
|||
user_id: i64,
|
||||
},
|
||||
UsersAdd {
|
||||
username: String,
|
||||
username: Option<String>,
|
||||
tu: Option<PathBuf>,
|
||||
},
|
||||
UsersRemove {
|
||||
UsersRelease {
|
||||
user_id: i64,
|
||||
confirmed: bool,
|
||||
},
|
||||
UsersImport {
|
||||
username: String,
|
||||
UsersPurgeData {
|
||||
user_id: i64,
|
||||
confirmed: bool,
|
||||
},
|
||||
UsersCompleteDelete {
|
||||
user_id: i64,
|
||||
tu: Option<PathBuf>,
|
||||
confirmed: bool,
|
||||
},
|
||||
OmikronReconnect,
|
||||
IdentityRotate {
|
||||
|
|
@ -367,12 +393,29 @@ impl CliInvocation {
|
|||
Some(CliCommand::Users(users)) => match users.action {
|
||||
UsersAction::List => Command::UsersList,
|
||||
UsersAction::Show { user_id } => Command::UsersShow { user_id },
|
||||
UsersAction::Add { username } => Command::UsersAdd { username },
|
||||
UsersAction::Remove { user_id, yes } => Command::UsersRemove {
|
||||
UsersAction::Add { username, tu } => {
|
||||
if username.is_some() == tu.is_some() {
|
||||
return Err(
|
||||
"users add requires exactly one of <username> or --tu <PATH>".into(),
|
||||
);
|
||||
}
|
||||
Command::UsersAdd { username, tu }
|
||||
}
|
||||
UsersAction::Release { user_id, yes } => Command::UsersRelease {
|
||||
user_id,
|
||||
confirmed: resolve_confirmed(yes),
|
||||
},
|
||||
UsersAction::Import { username } => Command::UsersImport { username },
|
||||
UsersAction::Data {
|
||||
action: UserDataAction::Purge { user_id, yes },
|
||||
} => Command::UsersPurgeData {
|
||||
user_id,
|
||||
confirmed: resolve_confirmed(yes),
|
||||
},
|
||||
UsersAction::CompleteDelete { user_id, tu, yes } => Command::UsersCompleteDelete {
|
||||
user_id,
|
||||
tu,
|
||||
confirmed: resolve_confirmed(yes),
|
||||
},
|
||||
},
|
||||
Some(CliCommand::Omikron(omikron)) => match omikron.action {
|
||||
OmikronAction::Reconnect => Command::OmikronReconnect,
|
||||
|
|
@ -445,6 +488,7 @@ impl CliInvocation {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn help_text() -> String {
|
||||
Cli::command().render_long_help().to_string()
|
||||
}
|
||||
|
|
@ -528,7 +572,7 @@ mod tests {
|
|||
#[test]
|
||||
fn command_schema_drives_help_and_completion_paths() {
|
||||
let paths = CliInvocation::command_paths();
|
||||
assert!(paths.contains(&"users remove".to_owned()));
|
||||
assert!(paths.contains(&"users release".to_owned()));
|
||||
assert!(paths.contains(&"daemon install".to_owned()));
|
||||
let help = CliInvocation::help_text();
|
||||
assert!(help.contains("users"));
|
||||
|
|
@ -558,12 +602,7 @@ mod tests {
|
|||
#[test]
|
||||
fn parses_unconfirmed_destructive_commands_explicitly() {
|
||||
let invocation = CliInvocation::parse(["daemon".into(), "stop".into()]).unwrap();
|
||||
assert_eq!(
|
||||
invocation.command,
|
||||
Command::DaemonStop {
|
||||
confirmed: true
|
||||
}
|
||||
);
|
||||
assert_eq!(invocation.command, Command::DaemonStop { confirmed: true });
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -573,7 +612,8 @@ mod tests {
|
|||
assert_eq!(
|
||||
invocation.command,
|
||||
Command::UsersAdd {
|
||||
username: "alice".into()
|
||||
username: Some("alice".into()),
|
||||
tu: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -597,26 +637,24 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parses_users_remove_without_confirmation() {
|
||||
let invocation =
|
||||
CliInvocation::parse(["users".into(), "remove".into(), "42".into()]).unwrap();
|
||||
assert_eq!(
|
||||
invocation.command,
|
||||
Command::UsersRemove {
|
||||
user_id: 42,
|
||||
confirmed: true,
|
||||
}
|
||||
);
|
||||
fn rejects_ambiguous_users_remove() {
|
||||
let error =
|
||||
CliInvocation::parse(["users".into(), "remove".into(), "42".into()]).unwrap_err();
|
||||
assert!(error.contains("remove"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_users_remove_with_confirmation() {
|
||||
let invocation =
|
||||
CliInvocation::parse(["users".into(), "remove".into(), "42".into(), "--yes".into()])
|
||||
.unwrap();
|
||||
fn parses_users_release_with_confirmation() {
|
||||
let invocation = CliInvocation::parse([
|
||||
"users".into(),
|
||||
"release".into(),
|
||||
"42".into(),
|
||||
"--yes".into(),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
invocation.command,
|
||||
Command::UsersRemove {
|
||||
Command::UsersRelease {
|
||||
user_id: 42,
|
||||
confirmed: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -403,7 +403,9 @@ fn print_help() {
|
|||
println!(" users list List all users");
|
||||
println!(" users show <ID> Show user details");
|
||||
println!(" users add <NAME> Create a new user");
|
||||
println!(" users remove <ID> Remove a user (requires --yes)");
|
||||
println!(" users add --tu <PATH> Add an existing account credential");
|
||||
println!(" users data purge <ID> Purge hosted data (requires --yes)");
|
||||
println!(" users release <ID> Release this Iota (requires --yes)");
|
||||
println!(" omikron status Show Omikron connection status");
|
||||
println!(" omikron reconnect Reconnect to Omikron");
|
||||
println!(" identity rotate Rotate identity keys (requires --yes)");
|
||||
|
|
@ -451,7 +453,7 @@ fn print_help() {
|
|||
println!(" iota status Show daemon status");
|
||||
println!(" iota users list --output=json List users in JSON format");
|
||||
println!(" iota users add alice Create a user named 'alice'");
|
||||
println!(" iota users remove 42 --yes Remove user 42");
|
||||
println!(" iota users data purge 42 --yes Purge hosted data");
|
||||
println!(" iota config get --output=yaml Show config in YAML format");
|
||||
println!(" iota logs --limit 50 Show last 50 log entries");
|
||||
println!(" iota completions bash Generate bash completions");
|
||||
|
|
@ -564,12 +566,40 @@ async fn run_command(
|
|||
Command::Tasks => LocalRequest::ListTasks,
|
||||
Command::UsersList => LocalRequest::ListUsers,
|
||||
Command::UsersShow { user_id } => LocalRequest::GetUser { user_id },
|
||||
Command::UsersAdd { username } => LocalRequest::CreateUser { username },
|
||||
Command::UsersRemove {
|
||||
Command::UsersAdd { username: Some(username), tu: None } => LocalRequest::CreateUser { username },
|
||||
Command::UsersAdd { username: None, tu: Some(path) } => {
|
||||
let contents = std::fs::read_to_string(&path)
|
||||
.map_err(|error| StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())))?;
|
||||
iota_util::tu::TuCredential::parse(&contents)
|
||||
.map_err(|error| StartupError::InvalidCommand(format!("Invalid credential {}: {error}", path.display())))?;
|
||||
LocalRequest::AttachUserFromTu { credential: iota_ipc::SecretString(contents) }
|
||||
}
|
||||
Command::UsersAdd { .. } => {
|
||||
return Err(StartupError::InvalidCommand(
|
||||
"users add requires exactly one of <username> or --tu <PATH>".into(),
|
||||
));
|
||||
}
|
||||
Command::UsersRelease {
|
||||
user_id,
|
||||
confirmed: true,
|
||||
} => LocalRequest::RemoveUser { user_id },
|
||||
Command::UsersImport { username } => LocalRequest::ImportUser { username },
|
||||
} => LocalRequest::ReleaseUser { user_id },
|
||||
Command::UsersPurgeData { user_id, confirmed: true } => LocalRequest::PurgeUserData { user_id },
|
||||
Command::UsersCompleteDelete { user_id, tu, confirmed: true } => {
|
||||
let credential = match tu {
|
||||
Some(path) => {
|
||||
let contents = std::fs::read_to_string(&path)
|
||||
.map_err(|error| StartupError::InvalidCommand(format!("Cannot read {}: {error}", path.display())))?;
|
||||
let parsed = iota_util::tu::TuCredential::parse(&contents)
|
||||
.map_err(|error| StartupError::InvalidCommand(format!("Invalid credential {}: {error}", path.display())))?;
|
||||
if parsed.user_id != user_id {
|
||||
return Err(StartupError::InvalidCommand("credential user ID does not match complete-delete target".into()));
|
||||
}
|
||||
Some(iota_ipc::SecretString(contents))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
LocalRequest::CompleteDeleteUser { user_id, credential }
|
||||
}
|
||||
Command::OmikronReconnect => LocalRequest::ReconnectOmikron,
|
||||
Command::IdentityRotate { confirmed: true } => LocalRequest::RotateIotaIdentity,
|
||||
Command::RegenerateKeys { confirmed: true } => LocalRequest::RotateIotaIdentity,
|
||||
|
|
@ -588,9 +618,11 @@ async fn run_command(
|
|||
Command::Logs { limit } => LocalRequest::GetLogs { limit },
|
||||
Command::UpdateCheck => LocalRequest::CheckUpdate,
|
||||
Command::CommunityList => LocalRequest::ListCommunities,
|
||||
Command::UsersRemove {
|
||||
Command::UsersRelease {
|
||||
confirmed: false, ..
|
||||
}
|
||||
| Command::UsersPurgeData { confirmed: false, .. }
|
||||
| Command::UsersCompleteDelete { confirmed: false, .. }
|
||||
| Command::IdentityRotate { confirmed: false }
|
||||
| Command::RegenerateKeys { confirmed: false }
|
||||
| Command::DaemonRestart { confirmed: false }
|
||||
|
|
@ -689,6 +721,9 @@ async fn run_command(
|
|||
user_id
|
||||
);
|
||||
}
|
||||
ResponsePayload::UserDataPurged { user_id } => {
|
||||
println!("{} hosted data for {}. Account remains managed by this Iota.", cli_color::success(&color, "Purged"), user_id);
|
||||
}
|
||||
ResponsePayload::Acknowledged { message } => {
|
||||
println!("{}", message);
|
||||
}
|
||||
|
|
@ -935,6 +970,9 @@ fn render_table(payload: &ResponsePayload) {
|
|||
ResponsePayload::UserRemoved { user_id } => {
|
||||
println!("Removed user {}", user_id);
|
||||
}
|
||||
ResponsePayload::UserDataPurged { user_id } => {
|
||||
println!("Purged hosted data for {}", user_id);
|
||||
}
|
||||
ResponsePayload::Acknowledged { message } => {
|
||||
println!("{}", message);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue