feat(user): user management tool (#825)

* 用户管理工具

* 重构

* 改为多个bin文件入口

* bin文件的usage显示自身程序名而非固定程序名
This commit is contained in:
Jomo
2024-06-05 13:00:19 +08:00
committed by GitHub
parent 415e14e9c3
commit 03746da3d9
29 changed files with 2575 additions and 0 deletions

View File

@ -0,0 +1,908 @@
use super::info::{GAddInfo, GDelInfo, GModInfo, PasswdInfo, UAddInfo, UDelInfo, UModInfo};
use crate::{
error::error::{ErrorHandler, ExitStatus},
parser::cmd::{CmdOption, GroupCommand, PasswdCommand, UserCommand},
};
use std::{
collections::{HashMap, HashSet},
fs,
io::Write,
};
/// useradd命令检查器
#[derive(Debug)]
pub struct UAddCheck;
impl UAddCheck {
/// **校验解析后的useradd命令**
///
/// ## 参数
/// - `cmd`: 解析后的useradd命令
///
/// ## 返回
/// - `UAddInfo`: 校验后的信息
pub fn check(cmd: UserCommand) -> UAddInfo {
let mut info = UAddInfo::default();
info.username = cmd.username;
// 填充信息
for (option, arg) in cmd.options.iter() {
match option {
CmdOption::Shell => {
info.shell = arg.clone();
}
CmdOption::Comment => {
info.comment = arg.clone();
}
CmdOption::Uid => {
info.uid = arg.clone();
}
CmdOption::Group => {
info.group = arg.clone();
}
CmdOption::Gid => {
info.gid = arg.clone();
}
CmdOption::Dir => {
info.home_dir = arg.clone();
}
_ => {
let op: &str = option.clone().into();
ErrorHandler::error_handle(
format!("Unimplemented option: {}", op),
ExitStatus::InvalidCmdSyntax,
);
}
}
}
// 完善用户信息
if info.username.is_empty() {
ErrorHandler::error_handle("Invalid username".to_string(), ExitStatus::InvalidArg);
}
if info.uid.is_empty() {
ErrorHandler::error_handle("Uid is required".to_string(), ExitStatus::InvalidCmdSyntax);
}
if info.comment.is_empty() {
info.comment = info.username.clone() + ",,,";
}
if info.home_dir.is_empty() {
let home_dir = format!("/home/{}", info.username.clone());
info.home_dir = home_dir;
}
if info.shell.is_empty() {
info.shell = "/bin/NovaShell".to_string();
}
// 校验终端是否有效
check_shell(&info.shell);
// 校验是否有重复用户名和用户id
scan_passwd(
PasswdField {
username: Some(info.username.clone()),
uid: Some(info.uid.clone()),
},
false,
);
// 判断group和gid是否有效
Self::check_group_gid(&mut info);
info
}
/// 检查组名、组id是否有效如果组名不存在则创建新的用户组
fn check_group_gid(info: &mut UAddInfo) {
if info.group.is_empty() && info.gid.is_empty() {
ErrorHandler::error_handle(
"user must belong to a group".to_string(),
ExitStatus::InvalidCmdSyntax,
);
}
let r = fs::read_to_string("/etc/group");
let mut max_gid: u32 = 0;
match r {
Ok(content) => {
for line in content.lines() {
let data: Vec<&str> = line.split(":").collect();
let (groupname, gid) = (data[0].to_string(), data[2].to_string());
if !info.group.is_empty() && info.group == groupname {
if !info.gid.is_empty() && info.gid != gid {
ErrorHandler::error_handle(
format!("The gid of the group [{}] isn't {}", info.group, info.gid),
ExitStatus::InvalidArg,
)
} else if info.gid.is_empty() || info.gid == gid {
info.gid = gid;
return;
}
}
if !info.gid.is_empty() && info.gid == gid {
if !info.group.is_empty() && info.group != groupname {
ErrorHandler::error_handle(
format!("The gid of the group [{}] isn't {}", info.group, info.gid),
ExitStatus::InvalidArg,
)
} else if info.group.is_empty() || info.group == groupname {
info.group = groupname;
return;
}
}
max_gid = max_gid.max(u32::from_str_radix(data[2], 10).unwrap());
}
}
Err(_) => {
ErrorHandler::error_handle(
"Can't read file: /etc/group".to_string(),
ExitStatus::GroupFile,
);
}
}
// 没有对应的用户组,默认创建新的用户组
let mut groupname = info.username.clone();
let mut gid = (max_gid + 1).to_string();
if !info.group.is_empty() {
groupname = info.group.clone();
} else {
info.group = groupname.clone();
}
if !info.gid.is_empty() {
gid = info.gid.clone();
} else {
info.gid = gid.clone();
}
let mut success = true;
let r = std::process::Command::new("/bin/groupadd")
.arg("-g")
.arg(gid.clone())
.arg(groupname)
.status();
if let Ok(exit_status) = r {
if exit_status.code() != Some(0) {
success = false;
}
} else {
success = false;
}
if !success {
ErrorHandler::error_handle("groupadd failed".to_string(), ExitStatus::GroupaddFail);
}
}
}
/// userdel命令检查器
#[derive(Debug)]
pub struct UDelCheck;
impl UDelCheck {
/// **校验userdel命令**
///
/// ## 参数
/// - `cmd`: userdel命令
///
/// ## 返回
/// - `UDelInfo`: 校验后的用户信息
pub fn check(cmd: UserCommand) -> UDelInfo {
let mut info = UDelInfo::default();
info.username = cmd.username;
// 检查用户是否存在
scan_passwd(
PasswdField {
username: Some(info.username.clone()),
uid: None,
},
true,
);
if let Some(_) = cmd.options.get(&CmdOption::Remove) {
info.home = Some(Self::home(&info.username));
}
info
}
/// 获取用户家目录
fn home(username: &String) -> String {
let mut home = String::new();
match std::fs::read_to_string("/etc/passwd") {
Ok(data) => {
for line in data.lines() {
let data = line.split(':').collect::<Vec<&str>>();
if data[0] == username {
home = data[5].to_string();
break;
}
}
}
Err(_) => {
ErrorHandler::error_handle(
"Can't read file: /etc/passwd".to_string(),
ExitStatus::PasswdFile,
);
}
}
home
}
}
/// usermod命令检查器
#[derive(Debug)]
pub struct UModCheck;
impl UModCheck {
/// **校验usermod命令**
///
/// ## 参数
/// - `cmd`: usermod命令
///
/// ## 返回
/// - `UModInfo`: 校验后的用户信息
pub fn check(cmd: UserCommand) -> UModInfo {
let mut info = Self::parse_options(&cmd.options);
info.username = cmd.username;
// 校验shell是否有效
if let Some(shell) = &info.new_shell {
check_shell(shell);
}
// 校验new_home是否有效
if let Some(new_home) = &info.new_home {
Self::check_home(new_home);
}
// 校验用户是否存在
scan_passwd(
PasswdField {
username: Some(info.username.clone()),
uid: None,
},
true,
);
// 校验new_name、new_uid是否有效
scan_passwd(
PasswdField {
username: info.new_name.clone(),
uid: info.new_uid.clone(),
},
false,
);
// 校验groups、new_gid是否有效
scan_group(
GroupField {
groups: info.groups.clone(),
gid: info.new_gid.clone(),
},
true,
);
info
}
/// **校验home目录是否有效**
///
/// ## 参数
/// - `home`: home目录路径
fn check_home(home: &String) {
if fs::File::open(home).is_ok() {
ErrorHandler::error_handle(format!("{} already exists", home), ExitStatus::InvalidArg);
}
}
/// **解析options**
///
/// ## 参数
/// - `options`: 命令选项
///
/// ## 返回
/// - `UModInfo`: 用户信息
fn parse_options(options: &HashMap<CmdOption, String>) -> UModInfo {
let mut info = UModInfo::default();
for (option, arg) in options {
match option {
CmdOption::Append => {
info.groups = Some(arg.split(",").map(|s| s.to_string()).collect());
}
CmdOption::Comment => {
info.new_comment = Some(arg.clone());
}
CmdOption::Dir => {
info.new_home = Some(arg.clone());
}
CmdOption::Gid => {
info.new_gid = Some(arg.clone());
}
CmdOption::Login => {
info.new_name = Some(arg.clone());
}
CmdOption::Shell => {
info.new_shell = Some(arg.clone());
}
CmdOption::Uid => {
info.new_uid = Some(arg.clone());
}
_ => ErrorHandler::error_handle(
"Invalid option".to_string(),
ExitStatus::InvalidCmdSyntax,
),
}
}
info
}
}
/// passwd命令检查器
#[derive(Debug)]
pub struct PasswdCheck;
impl PasswdCheck {
/// **校验passwd命令**
///
/// ## 参数
/// - `cmd`: passwd命令
///
/// ## 返回
/// - `PasswdInfo`: 校验后的信息
pub fn check(cmd: PasswdCommand) -> PasswdInfo {
let uid = unsafe { libc::geteuid().to_string() };
let cur_username = Self::cur_username(uid.clone());
let mut to_change_username = String::new();
if let Some(username) = cmd.username {
to_change_username = username.clone();
// 不是root用户不能修改别人的密码
if uid != "0" && cur_username != username {
ErrorHandler::error_handle(
"You can't change password for other users".to_string(),
ExitStatus::PermissionDenied,
);
}
// 检验待修改用户是否存在
scan_passwd(
PasswdField {
username: Some(username.clone()),
uid: None,
},
true,
);
}
let mut new_password = String::new();
match uid.as_str() {
"0" => {
if to_change_username.is_empty() {
to_change_username = cur_username;
}
print!("New password: ");
std::io::stdout().flush().unwrap();
std::io::stdin().read_line(&mut new_password).unwrap();
new_password = new_password.trim().to_string();
let mut check_password = String::new();
print!("\nRe-enter new password: ");
std::io::stdout().flush().unwrap();
std::io::stdin().read_line(&mut check_password).unwrap();
check_password = check_password.trim().to_string();
if new_password != check_password {
ErrorHandler::error_handle(
"\nThe two passwords that you entered do not match.".to_string(),
ExitStatus::InvalidArg,
)
}
}
_ => {
to_change_username = cur_username.clone();
print!("Old password: ");
std::io::stdout().flush().unwrap();
let mut old_password = String::new();
std::io::stdin().read_line(&mut old_password).unwrap();
old_password = old_password.trim().to_string();
Self::check_password(cur_username, old_password);
print!("\nNew password: ");
std::io::stdout().flush().unwrap();
std::io::stdin().read_line(&mut new_password).unwrap();
new_password = new_password.trim().to_string();
print!("\nRe-enter new password: ");
std::io::stdout().flush().unwrap();
let mut check_password = String::new();
std::io::stdin().read_line(&mut check_password).unwrap();
check_password = check_password.trim().to_string();
if new_password != check_password {
println!("{}", new_password);
ErrorHandler::error_handle(
"\nThe two passwords that you entered do not match.".to_string(),
ExitStatus::InvalidArg,
)
}
}
};
PasswdInfo {
username: to_change_username,
new_password,
}
}
/// **获取uid对应的用户名**
///
/// ## 参数
/// - `uid`: 用户id
///
/// ## 返回
/// 用户名
fn cur_username(uid: String) -> String {
let r = fs::read_to_string("/etc/passwd");
let mut cur_username = String::new();
match r {
Ok(content) => {
for line in content.lines() {
let field = line.split(":").collect::<Vec<&str>>();
if uid == field[2] {
cur_username = field[0].to_string();
}
}
}
Err(_) => {
ErrorHandler::error_handle(
"Can't read /etc/passwd".to_string(),
ExitStatus::PasswdFile,
);
}
}
cur_username
}
/// **校验密码**
///
/// ## 参数
/// - `username`: 用户名
/// - `password`: 密码
fn check_password(username: String, password: String) {
let r = fs::read_to_string("/etc/shadow");
match r {
Ok(content) => {
for line in content.lines() {
let field = line.split(":").collect::<Vec<&str>>();
if username == field[0] {
if password != field[1] {
ErrorHandler::error_handle(
"Password error".to_string(),
ExitStatus::InvalidArg,
);
} else {
return;
}
}
}
}
Err(_) => {
ErrorHandler::error_handle(
"Can't read /etc/shadow".to_string(),
ExitStatus::ShadowFile,
);
}
}
}
}
/// groupadd命令检查器
#[derive(Debug)]
pub struct GAddCheck;
impl GAddCheck {
/// **校验groupadd命令**
///
/// ## 参数
/// - `cmd`: groupadd命令
///
/// ## 返回
/// - `GAddInfo`: 校验后的组信息
pub fn check(cmd: GroupCommand) -> GAddInfo {
let mut info = GAddInfo {
groupname: cmd.groupname.clone(),
gid: String::new(),
passwd: None,
};
if info.groupname.is_empty() {
ErrorHandler::error_handle("groupname is required".to_string(), ExitStatus::InvalidArg);
}
if let Some(gid) = cmd.options.get(&CmdOption::Gid) {
info.gid = gid.clone();
} else {
ErrorHandler::error_handle("gid is required".to_string(), ExitStatus::InvalidArg);
}
if let Some(passwd) = cmd.options.get(&CmdOption::Passwd) {
info.passwd = Some(passwd.clone());
}
// 检查组名或组id是否已存在
scan_group(
GroupField {
groups: Some(vec![info.groupname.clone()]),
gid: Some(info.gid.clone()),
},
false,
);
info
}
}
/// groupdel命令检查器
#[derive(Debug)]
pub struct GDelCheck;
impl GDelCheck {
/// **校验groupdel命令**
///
/// ## 参数
/// - `cmd`: groupdel命令
///
/// ## 返回
/// - `GDelInfo`: 校验后的组信息
pub fn check(cmd: GroupCommand) -> GDelInfo {
if let Some(gid) = check_groupname(cmd.groupname.clone()) {
// 检查group是不是某个用户的主组如果是的话则不能删除
Self::is_main_group(gid);
} else {
// 用户组不存在
ErrorHandler::error_handle(
format!("group:[{}] doesn't exist", cmd.groupname),
ExitStatus::GroupNotExist,
);
}
GDelInfo {
groupname: cmd.groupname,
}
}
/// **检查该组是否为某个用户的主用户组**
///
/// ## 参数
/// - `gid`: 组id
///
/// ## 返回
/// Some(gid): 组id
/// None
fn is_main_group(gid: String) {
// 读取/etc/passwd文件
let r = fs::read_to_string("/etc/passwd");
match r {
Ok(content) => {
for line in content.lines() {
let field = line.split(":").collect::<Vec<&str>>();
if field[3] == gid {
ErrorHandler::error_handle(
format!(
"groupdel failed: group is main group of user:[{}]",
field[0]
),
ExitStatus::InvalidArg,
)
}
}
}
Err(_) => {
ErrorHandler::error_handle(
"Can't read file: /etc/passwd".to_string(),
ExitStatus::PasswdFile,
);
}
}
}
}
/// groupmod命令检查器
#[derive(Debug)]
pub struct GModCheck;
impl GModCheck {
/// **校验groupmod命令**
///
/// ## 参数
/// - `cmd`: groupmod命令
///
/// ## 返回
/// - `GModInfo`: 校验后的组信息
pub fn check(cmd: GroupCommand) -> GModInfo {
let mut info = GModInfo::default();
info.groupname = cmd.groupname;
if let Some(new_groupname) = cmd.options.get(&CmdOption::NewGroupName) {
info.new_groupname = Some(new_groupname.clone());
}
if let Some(new_gid) = cmd.options.get(&CmdOption::Gid) {
info.new_gid = Some(new_gid.clone());
}
Self::check_group_file(&mut info);
info
}
/// 查看groupname是否存在同时检测new_gid、new_groupname是否重复
fn check_group_file(info: &mut GModInfo) {
let mut is_group_exist = false;
let r = fs::read_to_string("/etc/group");
match r {
Ok(content) => {
for line in content.lines() {
let field = line.split(':').collect::<Vec<&str>>();
if field[0] == info.groupname {
is_group_exist = true;
info.gid = field[2].to_string();
}
if let Some(new_gid) = &info.new_gid {
if new_gid == field[2] {
ErrorHandler::error_handle(
format!("gid:[{}] is already used", new_gid),
ExitStatus::InvalidArg,
);
}
}
if let Some(new_groupname) = &info.new_groupname {
if new_groupname == field[0] {
ErrorHandler::error_handle(
format!("groupname:[{}] is already used", new_groupname),
ExitStatus::InvalidArg,
);
}
}
}
}
Err(_) => ErrorHandler::error_handle(
"Can't read file: /etc/group".to_string(),
ExitStatus::GroupFile,
),
}
if !is_group_exist {
ErrorHandler::error_handle(
format!("groupname:[{}] doesn't exist", info.groupname),
ExitStatus::GroupNotExist,
);
}
}
}
/// passwd文件待校验字段
pub struct PasswdField {
username: Option<String>,
uid: Option<String>,
}
/// group文件待校验字段
pub struct GroupField {
groups: Option<Vec<String>>,
gid: Option<String>,
}
/// **校验uid**
///
/// ## 参数
/// - `passwd_field`: passwd文件字段
/// - `should_exist`: 是否应该存在
fn scan_passwd(passwd_field: PasswdField, should_exist: bool) {
let mut username_check = false;
let mut uid_check = false;
match fs::read_to_string("/etc/passwd") {
Ok(content) => {
for line in content.lines() {
let field = line.split(':').collect::<Vec<&str>>();
if let Some(uid) = &passwd_field.uid {
// uid必须是有效的数字
let r = uid.parse::<u32>();
if r.is_err() {
ErrorHandler::error_handle(
format!("Uid {} is invalid", uid),
ExitStatus::InvalidArg,
);
}
if field[2] == uid {
uid_check = true;
// username如果不用校验或者被校验过了才可以return
if should_exist && (passwd_field.username.is_none() || username_check) {
return;
} else {
ErrorHandler::error_handle(
format!("UID {} already exists", uid),
ExitStatus::UidInUse,
);
}
}
}
if let Some(username) = &passwd_field.username {
if field[0] == username {
username_check = true;
// uid如果不用校验或者被校验过了才可以return
if should_exist && (passwd_field.uid.is_none() || uid_check) {
return;
} else {
ErrorHandler::error_handle(
format!("Username {} already exists", username),
ExitStatus::UsernameInUse,
);
}
}
}
}
if should_exist {
if let Some(uid) = &passwd_field.uid {
if !uid_check {
ErrorHandler::error_handle(
format!("UID {} doesn't exist", uid),
ExitStatus::InvalidArg,
);
}
}
if let Some(username) = &passwd_field.username {
if !username_check {
ErrorHandler::error_handle(
format!("User {} doesn't exist", username),
ExitStatus::InvalidArg,
);
}
}
}
}
Err(_) => ErrorHandler::error_handle(
"Can't read file: /etc/passwd".to_string(),
ExitStatus::PasswdFile,
),
}
}
/// **校验gid**
///
/// ## 参数
/// - `group_field`: group文件字段
/// - `should_exist`: 是否应该存在
fn scan_group(group_field: GroupField, should_exist: bool) {
let mut gid_check = false;
let mut set1 = HashSet::new();
let mut set2 = HashSet::new();
if let Some(groups) = group_field.groups.clone() {
set2.extend(groups.into_iter());
}
match fs::read_to_string("/etc/group") {
Ok(content) => {
for line in content.lines() {
let field = line.split(':').collect::<Vec<&str>>();
if let Some(gid) = &group_field.gid {
// gid必须是有效的数字
let r = gid.parse::<u32>();
if r.is_err() {
ErrorHandler::error_handle(
format!("Gid {} is invalid", gid),
ExitStatus::InvalidArg,
);
}
if field[2] == gid {
gid_check = true;
if should_exist && group_field.groups.is_none() {
return;
} else {
ErrorHandler::error_handle(
format!("GID {} already exists", gid),
ExitStatus::InvalidArg,
);
}
}
}
// 统计所有组
set1.insert(field[0].to_string());
}
if should_exist {
if let Some(gid) = &group_field.gid {
if !gid_check {
ErrorHandler::error_handle(
format!("GID {} doesn't exist", gid),
ExitStatus::InvalidArg,
);
}
}
if group_field.groups.is_some() {
let mut non_exist_group = Vec::new();
for group in set2.iter() {
if !set1.contains(group) {
non_exist_group.push(group.clone());
}
}
if non_exist_group.len() > 0 {
ErrorHandler::error_handle(
format!("group: {} doesn't exist", non_exist_group.join(",")),
ExitStatus::GroupNotExist,
);
}
}
}
}
Err(_) => ErrorHandler::error_handle(
"Can't read file: /etc/group".to_string(),
ExitStatus::GroupFile,
),
}
}
/// **校验shell是否有效**
///
/// ## 参数
/// - `shell`: shell路径
fn check_shell(shell: &String) {
if let Ok(file) = fs::File::open(shell.clone()) {
if !file.metadata().unwrap().is_file() {
ErrorHandler::error_handle(format!("{} is not a file", shell), ExitStatus::InvalidArg);
}
} else {
ErrorHandler::error_handle(format!("{} doesn't exist", shell), ExitStatus::InvalidArg);
}
}
/// **校验组名,判断该用户组是否存在,以及成员是否为空**
///
/// ## 参数
/// - `groupname`: 组名
///
/// ## 返回
/// Some(gid): 组id
/// None
fn check_groupname(groupname: String) -> Option<String> {
let r = fs::read_to_string("/etc/group");
match r {
Ok(content) => {
for line in content.lines() {
let field = line.split(":").collect::<Vec<&str>>();
let users = field[3].split(",").collect::<Vec<&str>>();
let filter_users = users
.iter()
.filter(|&x| !x.is_empty())
.collect::<Vec<&&str>>();
if field[0] == groupname {
if filter_users.is_empty() {
return Some(field[2].to_string());
} else {
ErrorHandler::error_handle(
format!("group:[{}] is not empty, unable to delete", groupname),
ExitStatus::InvalidArg,
)
}
}
}
}
Err(_) => {
ErrorHandler::error_handle(
"Can't read file: /etc/group".to_string(),
ExitStatus::GroupFile,
);
}
}
None
}

View File

@ -0,0 +1,95 @@
#[derive(Debug, Default, Clone)]
/// useradd的信息
pub struct UAddInfo {
/// 用户名
pub username: String,
pub uid: String,
pub gid: String,
/// 所在组的组名
pub group: String,
/// 用户描述信息
pub comment: String,
/// 主目录
pub home_dir: String,
/// 终端程序名
pub shell: String,
}
impl From<UAddInfo> for String {
fn from(info: UAddInfo) -> Self {
format!(
"{}::{}:{}:{}:{}:{}\n",
info.username, info.uid, info.gid, info.comment, info.home_dir, info.shell
)
}
}
#[derive(Debug, Default, Clone)]
/// userdel的信息
pub struct UDelInfo {
pub username: String,
pub home: Option<String>,
}
#[derive(Debug, Default, Clone)]
/// usermod的信息
pub struct UModInfo {
pub username: String,
pub groups: Option<Vec<String>>,
pub new_comment: Option<String>,
pub new_home: Option<String>,
pub new_gid: Option<String>,
pub new_group: Option<String>,
pub new_name: Option<String>,
pub new_shell: Option<String>,
pub new_uid: Option<String>,
}
#[derive(Debug, Default, Clone)]
/// passwd的信息
pub struct PasswdInfo {
pub username: String,
pub new_password: String,
}
#[derive(Debug, Default, Clone)]
/// groupadd的信息
pub struct GAddInfo {
pub groupname: String,
pub gid: String,
pub passwd: Option<String>,
}
impl GAddInfo {
pub fn to_string_group(&self) -> String {
let mut passwd = String::from("");
if self.passwd.is_some() {
passwd = "x".to_string();
}
format!("{}:{}:{}:\n", self.groupname, passwd, self.gid)
}
pub fn to_string_gshadow(&self) -> String {
let mut passwd = String::from("!");
if let Some(gpasswd) = &self.passwd {
passwd = gpasswd.clone();
}
format!("{}:{}::\n", self.groupname, passwd)
}
}
#[derive(Debug, Default, Clone)]
/// groupdel的信息
pub struct GDelInfo {
pub groupname: String,
}
#[derive(Debug, Default, Clone)]
/// groupmod的信息
pub struct GModInfo {
pub groupname: String,
pub gid: String,
pub new_groupname: Option<String>,
pub new_gid: Option<String>,
}

View File

@ -0,0 +1,3 @@
#![allow(dead_code)]
pub mod check;
pub mod info;