diff --git a/user/apps/user-manage/.gitignore b/user/apps/user-manage/.gitignore new file mode 100644 index 000000000..afe59cbbe --- /dev/null +++ b/user/apps/user-manage/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/install \ No newline at end of file diff --git a/user/apps/user-manage/Cargo.toml b/user/apps/user-manage/Cargo.toml new file mode 100644 index 000000000..c3e73f109 --- /dev/null +++ b/user/apps/user-manage/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "user_manage_tool" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "useradd" +path = "src/cmd/useradd.rs" + +[[bin]] +name = "userdel" +path = "src/cmd/userdel.rs" + +[[bin]] +name = "usermod" +path = "src/cmd/usermod.rs" + +[[bin]] +name = "passwd" +path = "src/cmd/passwd.rs" + +[[bin]] +name = "groupadd" +path = "src/cmd/groupadd.rs" + +[[bin]] +name = "groupdel" +path = "src/cmd/groupdel.rs" + +[[bin]] +name = "groupmod" +path = "src/cmd/groupmod.rs" + +[dependencies] +libc = "0.2.153" +lazy_static = "1.4.0" diff --git a/user/apps/user-manage/Makefile b/user/apps/user-manage/Makefile new file mode 100644 index 000000000..d018fdf34 --- /dev/null +++ b/user/apps/user-manage/Makefile @@ -0,0 +1,47 @@ +# The toolchain we use. +# You can get it by running DragonOS' `tools/bootstrap.sh` +TOOLCHAIN="+nightly-2023-08-15-x86_64-unknown-linux-gnu" +RUSTFLAGS+="" + +ifdef DADK_CURRENT_BUILD_DIR +# 如果是在dadk中编译,那么安装到dadk的安装目录中 + INSTALL_DIR = $(DADK_CURRENT_BUILD_DIR) +else +# 如果是在本地编译,那么安装到当前目录下的install目录中 + INSTALL_DIR = ./install +endif + + +ifeq ($(ARCH), x86_64) + export RUST_TARGET=x86_64-unknown-linux-musl +else ifeq ($(ARCH), riscv64) + export RUST_TARGET=riscv64gc-unknown-linux-gnu +else +# 默认为x86_86,用于本地编译 + export RUST_TARGET=x86_64-unknown-linux-musl +endif + +build: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) build --target $(RUST_TARGET) + +run-dragonreach: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) run --target $(RUST_TARGET) --bin DragonReach + +clean: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) clean + +build-release: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) build --target $(RUST_TARGET) --release + +clean-release: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) clean --target $(RUST_TARGET) --release + +fmt: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) fmt + +fmt-check: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) fmt --check + +.PHONY: install +install: + RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) install --target $(RUST_TARGET) --path . --no-track --root $(INSTALL_DIR) --force \ No newline at end of file diff --git a/user/apps/user-manage/README.md b/user/apps/user-manage/README.md new file mode 100644 index 000000000..510189013 --- /dev/null +++ b/user/apps/user-manage/README.md @@ -0,0 +1,142 @@ +## useradd + +- usage:添加用户 + + > useradd [options] username + + useradd -c \ -d \ -G \ -g \ -s \ -u \ username + +- 参数说明: + + - 选项: + -c comment 指定一段注释性描述 + -d 目录 指定用户主目录,如果不存在,则创建该目录 + -G 用户组 指定用户所属的用户组 + -g 组id + -s Shell 文件 指定用户的登录 Shell + -u 用户号 指定用户的用户号 + + - 用户名: + 指定新账号的登录名。 + +- 更新文件: + > /etc/passwd + > /etc/shadow + > /etc/group + > /etc/gshadow + +## userdel + +- usage:删除用户 + + > userdel [options] username + + userdel -r username + +- 选项: + -r 连同用户主目录一起删除。 + +- 更新文件: + > /etc/passwd + > /etc/shadow + > /etc/group + +## usermod + +- usage:修改用户 + + > usermod [options] username + + usermod -a -G<组 1,组 2,...> -c<备注> -d<登入目录> -G<组名> -l<名称> -s<登入终端> -u<用户 id> username + +- 选项: + -a -G<组 1,组 2,...> 将用户添加到其它组中 + -c<备注>  修改用户帐号的备注文字。 + -d 登入目录>  修改用户登入时的目录。 + -G<组名>  修改用户所属的群组。 + -l<名称>  修改用户名称。 + -s\  修改用户登入后所使用的 shell。 + -u\  修改用户 ID。 + +- 更新文件: + > /etc/passwd + > /etc/shadow + > /etc/group + > /etc/gshadow + +## passwd + +- usage:设置密码 + + > 普通用户: passwd + > root 用户: passwd username + + 普通用户只能修改自己的密码,因此不需要指定用户名。 + +- 更新文件 + > /etc/shadow + > /etc/passwd + +## groupadd + +- usage:添加用户组 + + > groupadd [options] groupname + + groupadd -g\ -p\ groupname + +- 选项: + -g\ 指定组 id + -p 设置密码 + +- 更新文件 + > /etc/group + > /etc/gshadow + +## groupdel + +- usage:删除用户组 + + > groupdel groupname + + groupdel \ + +- 注意事项: + 只有当用户组的组成员为空时才可以删除该组 + +- 更新文件 + > /etc/group + > /etc/gshadow + +## groupmod + +- usage:修改用户组信息 + + > groupmod [options] groupname + + groupadd -g\ -n\ groupname + +- 选项: + -g 设置新 gid + -n 设置新组名 + +- 更新文件 + > /etc/group + > /etc/gshadow + > /etc/passwd + +_/etc/passwd 文件格式:_ + +> 用户名:口令:用户标识号:组标识号:注释性描述:主目录:登录 Shell + +_/etc/shadow 文件格式:_ + +> 登录名:加密口令:最后一次修改时间:最小时间间隔:最大时间间隔:警告时间:不活动时间:失效时间:标志 + +_/etc/group 文件格式:_ + +> 组名:口令:组标识号:组内用户列表 + +_/etc/gshadow 文件格式:_ + +> 组名:组密码:组管理员名称:组成员 diff --git a/user/apps/user-manage/src/check/check.rs b/user/apps/user-manage/src/check/check.rs new file mode 100644 index 000000000..36754c928 --- /dev/null +++ b/user/apps/user-manage/src/check/check.rs @@ -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::>(); + 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) -> 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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, + uid: Option, +} + +/// group文件待校验字段 +pub struct GroupField { + groups: Option>, + gid: Option, +} + +/// **校验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::>(); + if let Some(uid) = &passwd_field.uid { + // uid必须是有效的数字 + let r = uid.parse::(); + 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::>(); + if let Some(gid) = &group_field.gid { + // gid必须是有效的数字 + let r = gid.parse::(); + 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 { + let r = fs::read_to_string("/etc/group"); + match r { + Ok(content) => { + for line in content.lines() { + let field = line.split(":").collect::>(); + let users = field[3].split(",").collect::>(); + let filter_users = users + .iter() + .filter(|&x| !x.is_empty()) + .collect::>(); + 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 +} diff --git a/user/apps/user-manage/src/check/info.rs b/user/apps/user-manage/src/check/info.rs new file mode 100644 index 000000000..b56fb952e --- /dev/null +++ b/user/apps/user-manage/src/check/info.rs @@ -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 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, +} + +#[derive(Debug, Default, Clone)] +/// usermod的信息 +pub struct UModInfo { + pub username: String, + pub groups: Option>, + pub new_comment: Option, + pub new_home: Option, + pub new_gid: Option, + pub new_group: Option, + pub new_name: Option, + pub new_shell: Option, + pub new_uid: Option, +} + +#[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, +} + +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, + pub new_gid: Option, +} diff --git a/user/apps/user-manage/src/check/mod.rs b/user/apps/user-manage/src/check/mod.rs new file mode 100644 index 000000000..8bea63796 --- /dev/null +++ b/user/apps/user-manage/src/check/mod.rs @@ -0,0 +1,3 @@ +#![allow(dead_code)] +pub mod check; +pub mod info; diff --git a/user/apps/user-manage/src/cmd/groupadd.rs b/user/apps/user-manage/src/cmd/groupadd.rs new file mode 100644 index 000000000..78e3ac7a5 --- /dev/null +++ b/user/apps/user-manage/src/cmd/groupadd.rs @@ -0,0 +1,45 @@ +use crate::{ + check::check::GAddCheck, + error::error::{ErrorHandler, ExitStatus}, + executor::executor::GAddExecutor, + parser::parser::GroupParser, +}; +use libc::geteuid; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + if unsafe { geteuid() } != 0 { + ErrorHandler::error_handle( + "permission denied (are you root?)".to_string(), + ExitStatus::PermissionDenied, + ) + } + + if args.len() < 2 { + ErrorHandler::error_handle( + format!("usage: {} [options] groupname", args[0]), + ExitStatus::InvalidCmdSyntax, + ); + } + + let cmd = GroupParser::parse(args); + let info = GAddCheck::check(cmd); + let groupname = info.groupname.clone(); + GAddExecutor::execute(info); + + println!("Add group [{}] successfully!", groupname); + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/cmd/groupdel.rs b/user/apps/user-manage/src/cmd/groupdel.rs new file mode 100644 index 000000000..ecb1a1739 --- /dev/null +++ b/user/apps/user-manage/src/cmd/groupdel.rs @@ -0,0 +1,45 @@ +use crate::{ + check::check::GDelCheck, + error::error::{ErrorHandler, ExitStatus}, + executor::executor::GDelExecutor, + parser::parser::GroupParser, +}; +use libc::geteuid; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + if unsafe { geteuid() } != 0 { + ErrorHandler::error_handle( + "permission denied (are you root?)".to_string(), + ExitStatus::PermissionDenied, + ) + } + + if args.len() < 2 { + ErrorHandler::error_handle( + format!("usage: {} [options] groupname", args[0]), + ExitStatus::InvalidCmdSyntax, + ); + } + + let cmd = GroupParser::parse(args); + let info = GDelCheck::check(cmd); + let groupname = info.groupname.clone(); + GDelExecutor::execute(info); + + println!("Delete group [{}] successfully!", groupname); + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/cmd/groupmod.rs b/user/apps/user-manage/src/cmd/groupmod.rs new file mode 100644 index 000000000..3d374132b --- /dev/null +++ b/user/apps/user-manage/src/cmd/groupmod.rs @@ -0,0 +1,46 @@ +use crate::{ + check::check::GModCheck, + error::error::{ErrorHandler, ExitStatus}, + executor::executor::GModExecutor, + parser::parser::GroupParser, +}; +use libc::geteuid; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + if unsafe { geteuid() } != 0 { + ErrorHandler::error_handle( + "permission denied (are you root?)".to_string(), + ExitStatus::PermissionDenied, + ) + } + + if args.len() < 2 { + ErrorHandler::error_handle( + format!("usage: {} [options] groupname", args[0]), + ExitStatus::InvalidCmdSyntax, + ); + } + + let cmd = GroupParser::parse(args); + if !cmd.options.is_empty() { + let info = GModCheck::check(cmd); + let groupname = info.groupname.clone(); + GModExecutor::execute(info); + println!("Modify group [{}] successfully!", groupname); + } + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/cmd/mod.rs b/user/apps/user-manage/src/cmd/mod.rs new file mode 100644 index 000000000..93886dea6 --- /dev/null +++ b/user/apps/user-manage/src/cmd/mod.rs @@ -0,0 +1,7 @@ +mod groupadd; +mod groupdel; +mod groupmod; +mod passwd; +mod useradd; +mod userdel; +mod usermod; diff --git a/user/apps/user-manage/src/cmd/passwd.rs b/user/apps/user-manage/src/cmd/passwd.rs new file mode 100644 index 000000000..204be906f --- /dev/null +++ b/user/apps/user-manage/src/cmd/passwd.rs @@ -0,0 +1,25 @@ +use crate::{ + check::check::PasswdCheck, error::error::ExitStatus, executor::executor::PasswdExecutor, + parser::parser::PasswdParser, +}; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + let cmd = PasswdParser::parse(args); + let info = PasswdCheck::check(cmd); + PasswdExecutor::execute(info); + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/cmd/useradd.rs b/user/apps/user-manage/src/cmd/useradd.rs new file mode 100644 index 000000000..4ad109a24 --- /dev/null +++ b/user/apps/user-manage/src/cmd/useradd.rs @@ -0,0 +1,44 @@ +use crate::{ + check::check::UAddCheck, + error::error::{ErrorHandler, ExitStatus}, + executor::executor::UAddExecutor, + parser::parser::UserParser, +}; +use libc::geteuid; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + if unsafe { geteuid() } != 0 { + ErrorHandler::error_handle( + "permission denied (are you root?)".to_string(), + ExitStatus::PermissionDenied, + ) + } + + if args.len() < 2 { + ErrorHandler::error_handle( + format!("usage: {} [options] username", args[0]), + ExitStatus::InvalidCmdSyntax, + ); + } + + let cmd = UserParser::parse(args); + let info = UAddCheck::check(cmd); + let username = info.username.clone(); + UAddExecutor::execute(info); + println!("Add user[{}] successfully!", username); + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/cmd/userdel.rs b/user/apps/user-manage/src/cmd/userdel.rs new file mode 100644 index 000000000..8d1a675fe --- /dev/null +++ b/user/apps/user-manage/src/cmd/userdel.rs @@ -0,0 +1,44 @@ +use crate::{ + check::check::UDelCheck, + error::error::{ErrorHandler, ExitStatus}, + executor::executor::UDelExecutor, + parser::parser::UserParser, +}; +use libc::geteuid; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + if unsafe { geteuid() } != 0 { + ErrorHandler::error_handle( + "permission denied (are you root?)".to_string(), + ExitStatus::PermissionDenied, + ) + } + + if args.len() < 2 { + ErrorHandler::error_handle( + format!("usage: {} [options] username", args[0]), + ExitStatus::InvalidCmdSyntax, + ); + } + + let cmd = UserParser::parse(args); + let info = UDelCheck::check(cmd); + let username = info.username.clone(); + UDelExecutor::execute(info); + println!("Delete user[{}] successfully!", username); + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/cmd/usermod.rs b/user/apps/user-manage/src/cmd/usermod.rs new file mode 100644 index 000000000..37d8ba585 --- /dev/null +++ b/user/apps/user-manage/src/cmd/usermod.rs @@ -0,0 +1,46 @@ +use crate::{ + check::check::UModCheck, + error::error::{ErrorHandler, ExitStatus}, + executor::executor::UModExecutor, + parser::parser::UserParser, +}; +use libc::geteuid; +use std::process::exit; + +#[path = "../check/mod.rs"] +mod check; +#[path = "../error/mod.rs"] +mod error; +#[path = "../executor/mod.rs"] +mod executor; +#[path = "../parser/mod.rs"] +mod parser; + +#[allow(dead_code)] +fn main() { + let args = std::env::args().collect::>(); + + if unsafe { geteuid() } != 0 { + ErrorHandler::error_handle( + "permission denied (are you root?)".to_string(), + ExitStatus::PermissionDenied, + ) + } + + if args.len() < 2 { + ErrorHandler::error_handle( + format!("usage: {} [options] username", args[0]), + ExitStatus::InvalidCmdSyntax, + ); + } + + let cmd = UserParser::parse(args); + if !cmd.options.is_empty() { + let info = UModCheck::check(cmd); + let username = info.username.clone(); + UModExecutor::execute(info); + println!("Modify user[{}] successfully!", username); + } + + exit(ExitStatus::Success as i32); +} diff --git a/user/apps/user-manage/src/error/error.rs b/user/apps/user-manage/src/error/error.rs new file mode 100644 index 000000000..cb299d5f6 --- /dev/null +++ b/user/apps/user-manage/src/error/error.rs @@ -0,0 +1,33 @@ +use std::process::exit; + +#[derive(Debug)] +pub enum ExitStatus { + Success = 0, + PasswdFile = 1, + InvalidCmdSyntax = 2, + InvalidArg = 3, + UidInUse = 4, + GroupNotExist = 6, + UsernameInUse = 9, + GroupFile = 10, + CreateHomeFail = 12, + PermissionDenied = -1, + ShadowFile = -2, + GshadowFile = -3, + GroupaddFail = -4, +} + +pub struct ErrorHandler; + +impl ErrorHandler { + /// **错误处理函数** + /// + /// ## 参数 + /// + /// - `error`错误信息 + /// - `exit_status` - 退出状态码 + pub fn error_handle(error: String, exit_status: ExitStatus) { + eprintln!("{error}"); + exit(exit_status as i32); + } +} diff --git a/user/apps/user-manage/src/error/mod.rs b/user/apps/user-manage/src/error/mod.rs new file mode 100644 index 000000000..7c82bf8fb --- /dev/null +++ b/user/apps/user-manage/src/error/mod.rs @@ -0,0 +1,2 @@ +#![allow(dead_code)] +pub mod error; diff --git a/user/apps/user-manage/src/executor/executor.rs b/user/apps/user-manage/src/executor/executor.rs new file mode 100644 index 000000000..fdfa50e0c --- /dev/null +++ b/user/apps/user-manage/src/executor/executor.rs @@ -0,0 +1,729 @@ +use crate::{ + check::info::{GAddInfo, GDelInfo, GModInfo, PasswdInfo, UAddInfo, UDelInfo, UModInfo}, + error::error::{ErrorHandler, ExitStatus}, +}; +use lazy_static::lazy_static; +use std::{ + fs::{self, File, OpenOptions}, + io::{Read, Seek, Write}, + sync::Mutex, +}; + +lazy_static! { + static ref GLOBAL_FILE: Mutex = Mutex::new(GlobalFile::new()); +} + +#[derive(Debug)] +pub struct GlobalFile { + passwd_file: File, + shadow_file: File, + group_file: File, + gshadow_file: File, +} + +impl GlobalFile { + pub fn new() -> Self { + let passwd = open_file("/etc/passwd"); + let shadow = open_file("/etc/shadow"); + let group = open_file("/etc/group"); + let gshadow = open_file("/etc/gshadow"); + Self { + passwd_file: passwd, + shadow_file: shadow, + group_file: group, + gshadow_file: gshadow, + } + } +} + +fn open_file(file_path: &str) -> File { + let r = OpenOptions::new() + .read(true) + .write(true) + .append(true) + .open(file_path); + + let exit_status = match file_path { + "/etc/group" => ExitStatus::GroupFile, + "/etc/gshadow" => ExitStatus::GshadowFile, + "/etc/passwd" => ExitStatus::PasswdFile, + "/etc/shadow" => ExitStatus::ShadowFile, + _ => ExitStatus::InvalidArg, + }; + + if r.is_err() { + ErrorHandler::error_handle(format!("Can't open file: {}", file_path), exit_status); + } + + r.unwrap() +} + +/// useradd执行器 +pub struct UAddExecutor; + +impl UAddExecutor { + /// **执行useradd** + /// + /// ## 参数 + /// - `info`: 用户信息 + pub fn execute(info: UAddInfo) { + // 创建用户home目录 + let home = info.home_dir.clone(); + let dir_builder = fs::DirBuilder::new(); + if dir_builder.create(home.clone()).is_err() { + ErrorHandler::error_handle( + format!("unable to create {}", home), + ExitStatus::CreateHomeFail, + ); + } + + Self::write_passwd_file(&info); + Self::write_shadow_file(&info); + Self::write_group_file(&info); + Self::write_gshadow_file(&info); + } + + /// 写入/etc/passwd文件:添加用户信息 + fn write_passwd_file(info: &UAddInfo) { + let userinfo: String = info.clone().into(); + GLOBAL_FILE + .lock() + .unwrap() + .passwd_file + .write_all(userinfo.as_bytes()) + .unwrap(); + } + + /// 写入/etc/group文件:将用户添加到对应用户组中 + fn write_group_file(info: &UAddInfo) { + if info.group == info.username { + return; + } + + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.group_file); + let mut new_content = String::new(); + for line in content.lines() { + let mut field = line.split(":").collect::>(); + let mut users = field.last().unwrap().split(",").collect::>(); + users = users + .into_iter() + .filter(|username| !username.is_empty()) + .collect::>(); + if field[0].eq(info.group.as_str()) && !users.contains(&info.username.as_str()) { + users.push(info.username.as_str()); + } + + let new_users = users.join(","); + field[3] = new_users.as_str(); + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + + guard.group_file.set_len(0).unwrap(); + guard.group_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.group_file.write_all(new_content.as_bytes()).unwrap(); + guard.group_file.flush().unwrap(); + } + + /// 写入/etc/shadow文件:添加用户口令相关信息 + fn write_shadow_file(info: &UAddInfo) { + let data = format!("{}::::::::\n", info.username,); + GLOBAL_FILE + .lock() + .unwrap() + .shadow_file + .write_all(data.as_bytes()) + .unwrap(); + } + + /// 写入/etc/gshadow文件:将用户添加到对应用户组中 + fn write_gshadow_file(info: &UAddInfo) { + if info.group == info.username { + return; + } + + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.gshadow_file); + let mut new_content = String::new(); + for line in content.lines() { + let mut field = line.split(":").collect::>(); + let mut users = field.last().unwrap().split(",").collect::>(); + users = users + .into_iter() + .filter(|username| !username.is_empty()) + .collect::>(); + if field[0].eq(info.group.as_str()) && !users.contains(&info.username.as_str()) { + users.push(info.username.as_str()); + } + + let new_users = users.join(","); + field[3] = new_users.as_str(); + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + guard.gshadow_file.set_len(0).unwrap(); + guard + .gshadow_file + .seek(std::io::SeekFrom::Start(0)) + .unwrap(); + guard + .gshadow_file + .write_all(new_content.as_bytes()) + .unwrap(); + guard.gshadow_file.flush().unwrap(); + } +} + +/// userdel执行器 +pub struct UDelExecutor; + +impl UDelExecutor { + /// **执行userdel** + /// + /// ## 参数 + /// - `info`: 用户信息 + pub fn execute(info: UDelInfo) { + // 移除home目录 + if let Some(home) = info.home.clone() { + std::fs::remove_dir_all(home).unwrap(); + } + + Self::update_passwd_file(&info); + Self::update_shadow_file(&info); + Self::update_group_file(&info); + Self::update_gshadow_file(&info); + } + + /// 更新/etc/passwd文件: 删除用户信息 + fn update_passwd_file(info: &UDelInfo) { + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.passwd_file); + let lines: Vec<&str> = content.lines().collect(); + let new_content = lines + .into_iter() + .filter(|&line| { + let field = line.split(':').collect::>(); + field[0] != info.username.as_str() + }) + .collect::>() + .join("\n"); + + guard.passwd_file.set_len(0).unwrap(); + guard.passwd_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.passwd_file.write_all(new_content.as_bytes()).unwrap(); + guard.passwd_file.flush().unwrap(); + } + + /// 更新/etc/group文件: 将用户从组中移除 + fn update_group_file(info: &UDelInfo) { + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.group_file); + let mut new_content = String::new(); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + let mut users = field.last().unwrap().split(",").collect::>(); + if users.contains(&info.username.as_str()) { + field.remove(field.len() - 1); + users.remove( + users + .iter() + .position(|&x| x == info.username.as_str()) + .unwrap(), + ); + let users = users.join(","); + field.push(&users.as_str()); + new_content.push_str(format!("{}\n", field.join(":").as_str()).as_str()); + } else { + new_content.push_str(format!("{}\n", field.join(":").as_str()).as_str()); + } + + guard.group_file.set_len(0).unwrap(); + guard.group_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.group_file.write_all(new_content.as_bytes()).unwrap(); + guard.group_file.flush().unwrap(); + } + } + + /// 更新/etc/shadow文件: 将用户信息删去 + fn update_shadow_file(info: &UDelInfo) { + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.shadow_file); + let lines: Vec<&str> = content.lines().collect(); + let new_content = lines + .into_iter() + .filter(|&line| !line.contains(&info.username)) + .collect::>() + .join("\n"); + + guard.shadow_file.set_len(0).unwrap(); + guard.shadow_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.shadow_file.write_all(new_content.as_bytes()).unwrap(); + guard.shadow_file.flush().unwrap(); + } + + /// 更新/etc/gshadow文件: 将用户从组中移除 + fn update_gshadow_file(info: &UDelInfo) { + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.gshadow_file); + let mut new_content = String::new(); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + let mut users = field.last().unwrap().split(",").collect::>(); + if users.contains(&info.username.as_str()) { + field.remove(field.len() - 1); + users.remove( + users + .iter() + .position(|&x| x == info.username.as_str()) + .unwrap(), + ); + let users = users.join(","); + field.push(&users.as_str()); + new_content.push_str(format!("{}\n", field.join(":").as_str()).as_str()); + } else { + new_content.push_str(format!("{}\n", field.join(":").as_str()).as_str()); + } + + guard.gshadow_file.set_len(0).unwrap(); + guard + .gshadow_file + .seek(std::io::SeekFrom::Start(0)) + .unwrap(); + guard + .gshadow_file + .write_all(new_content.as_bytes()) + .unwrap(); + guard.gshadow_file.flush().unwrap(); + } + } +} + +/// usermod执行器 +pub struct UModExecutor; + +impl UModExecutor { + /// **执行usermod** + /// + /// ## 参数 + /// - `info`: 用户信息 + pub fn execute(mut info: UModInfo) { + // 创建new_home + if let Some(new_home) = &info.new_home { + let dir_builder = fs::DirBuilder::new(); + if dir_builder.create(new_home.clone()).is_err() { + ErrorHandler::error_handle( + format!("unable to create {}", new_home), + ExitStatus::CreateHomeFail, + ); + } + } + + Self::update_passwd_file(&info); + Self::update_shadow_file(&info); + Self::update_group_file(&mut info); + Self::update_gshadow_file(&info); + } + + /// 更新/etc/passwd文件的username、uid、comment、home、shell + fn update_passwd_file(info: &UModInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.passwd_file); + for line in content.lines() { + let mut fields = line.split(':').collect::>(); + if fields[0] == info.username { + if let Some(new_username) = &info.new_name { + fields[0] = new_username; + } + if let Some(new_uid) = &info.new_uid { + fields[2] = new_uid; + } + if let Some(new_gid) = &info.new_gid { + fields[3] = new_gid; + } + if let Some(new_comment) = &info.new_comment { + fields[4] = new_comment; + } + if let Some(new_home) = &info.new_home { + fields[5] = new_home; + } + if let Some(new_shell) = &info.new_shell { + fields[6] = new_shell; + } + new_content.push_str(format!("{}\n", fields.join(":")).as_str()); + } else { + new_content.push_str(format!("{}\n", line).as_str()); + } + + guard.passwd_file.set_len(0).unwrap(); + guard.passwd_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.passwd_file.write_all(new_content.as_bytes()).unwrap(); + guard.passwd_file.flush().unwrap(); + } + } + + /// 更新/etc/group文件中各用户组中的用户 + fn update_group_file(info: &mut UModInfo) { + let mut name = info.username.clone(); + if let Some(new_name) = &info.new_name { + name = new_name.clone(); + } + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.group_file); + for line in content.lines() { + let mut fields = line.split(':').collect::>(); + let mut users = fields[3].split(",").collect::>(); + users = users + .into_iter() + .filter(|username| !username.is_empty()) + .collect::>(); + if let Some(idx) = users.iter().position(|&r| r == info.username) { + if let Some(gid) = &info.new_gid { + // 换组,将用户从当前组删去 + if gid != fields[2] { + users.remove(idx); + } else { + info.new_group = Some(fields[0].to_string()) + } + } else { + // 不换组但是要更新名字 + users[idx] = &name; + } + } + + if let Some(groups) = &info.groups { + if groups.contains(&fields[0].to_string()) && !users.contains(&name.as_str()) { + users.push(&name); + } + } + + let new_users = users.join(","); + fields[3] = new_users.as_str(); + new_content.push_str(format!("{}\n", fields.join(":")).as_str()); + } + + guard.group_file.set_len(0).unwrap(); + guard.group_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.group_file.write_all(new_content.as_bytes()).unwrap(); + guard.group_file.flush().unwrap(); + } + + /// 更新/etc/shadow文件的username + fn update_shadow_file(info: &UModInfo) { + if let Some(new_name) = &info.new_name { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.shadow_file); + for line in content.lines() { + let mut fields = line.split(':').collect::>(); + if fields[0] == info.username { + fields[0] = new_name; + new_content.push_str(format!("{}\n", fields.join(":")).as_str()); + } else { + new_content.push_str(format!("{}\n", line).as_str()); + } + } + + guard.shadow_file.set_len(0).unwrap(); + guard.shadow_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.shadow_file.write_all(new_content.as_bytes()).unwrap(); + guard.shadow_file.flush().unwrap(); + } + } + + /// 更新/etc/gshadow文件中各用户组中的用户 + fn update_gshadow_file(info: &UModInfo) { + let mut name = info.username.clone(); + if let Some(new_name) = &info.new_name { + name = new_name.clone(); + } + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.gshadow_file); + for line in content.lines() { + let mut fields = line.split(':').collect::>(); + let mut users = fields[3].split(",").collect::>(); + users = users + .into_iter() + .filter(|username| !username.is_empty()) + .collect::>(); + if let Some(idx) = users.iter().position(|&r| r == info.username) { + if let Some(group) = &info.new_group { + // 换组,将用户从当前组删去 + if group != fields[0] { + users.remove(idx); + } + } else { + // 不换组但是要更新名字 + users[idx] = &name; + } + } + + let tmp = format!(",{}", name); + if let Some(groups) = &info.groups { + if groups.contains(&fields[0].to_string()) && !users.contains(&name.as_str()) { + if users.is_empty() { + users.push(&name); + } else { + users.push(tmp.as_str()); + } + } + } + + let new_users = users.join(","); + fields[3] = new_users.as_str(); + new_content.push_str(format!("{}\n", fields.join(":")).as_str()); + } + + guard.gshadow_file.set_len(0).unwrap(); + guard + .gshadow_file + .seek(std::io::SeekFrom::Start(0)) + .unwrap(); + guard + .gshadow_file + .write_all(new_content.as_bytes()) + .unwrap(); + guard.gshadow_file.flush().unwrap(); + } +} + +/// passwd执行器 +pub struct PasswdExecutor; + +impl PasswdExecutor { + /// **执行passwd** + /// + /// ## 参数 + /// - `info`: 用户密码信息 + pub fn execute(info: PasswdInfo) { + Self::update_passwd_file(&info); + Self::update_shadow_file(&info); + } + + /// 更新/etc/passwd文件: 修改用户密码 + fn update_passwd_file(info: &PasswdInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.passwd_file); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + if field[0] == info.username { + if info.new_password.is_empty() { + field[1] = ""; + } else { + field[1] = "x"; + } + } + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + + guard.passwd_file.set_len(0).unwrap(); + guard.passwd_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.passwd_file.write_all(new_content.as_bytes()).unwrap(); + guard.passwd_file.flush().unwrap(); + } + + /// 更新/etc/shadow文件: 修改用户密码 + fn update_shadow_file(info: &PasswdInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.shadow_file); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + if field[0] == info.username { + field[1] = info.new_password.as_str(); + } + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + + guard.shadow_file.set_len(0).unwrap(); + guard.shadow_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.shadow_file.write_all(new_content.as_bytes()).unwrap(); + guard.shadow_file.flush().unwrap(); + } +} + +/// groupadd执行器 +pub struct GAddExecutor; + +impl GAddExecutor { + /// **执行groupadd** + /// + /// ## 参数 + /// - `info`: 组信息 + pub fn execute(info: GAddInfo) { + Self::write_group_file(&info); + Self::write_gshadow_file(&info); + } + + /// 写入/etc/group文件: 添加用户组信息 + fn write_group_file(info: &GAddInfo) { + GLOBAL_FILE + .lock() + .unwrap() + .group_file + .write_all(info.to_string_group().as_bytes()) + .unwrap() + } + + /// 写入/etc/gshadow文件: 添加用户组密码信息 + fn write_gshadow_file(info: &GAddInfo) { + GLOBAL_FILE + .lock() + .unwrap() + .gshadow_file + .write_all(info.to_string_gshadow().as_bytes()) + .unwrap(); + } +} + +/// groupdel执行器 +pub struct GDelExecutor; + +impl GDelExecutor { + /// **执行groupdel** + /// + /// ## 参数 + /// - `info`: 组信息 + pub fn execute(info: GDelInfo) { + Self::update_group_file(&info); + Self::update_gshadow_file(&info); + } + + /// 更新/etc/group文件:删除用户组 + pub fn update_group_file(info: &GDelInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.group_file); + for line in content.lines() { + let field = line.split(':').collect::>(); + if field[0] != info.groupname { + new_content.push_str(format!("{}\n", line).as_str()); + } + } + + guard.group_file.set_len(0).unwrap(); + guard.group_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.group_file.write_all(new_content.as_bytes()).unwrap(); + guard.group_file.flush().unwrap(); + } + + /// 更新/etc/gshadow文件:移除用户组 + pub fn update_gshadow_file(info: &GDelInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.gshadow_file); + for line in content.lines() { + let field = line.split(':').collect::>(); + if field[0] != info.groupname { + new_content.push_str(format!("{}\n", line).as_str()); + } + } + + guard.gshadow_file.set_len(0).unwrap(); + guard + .gshadow_file + .seek(std::io::SeekFrom::Start(0)) + .unwrap(); + guard + .gshadow_file + .write_all(new_content.as_bytes()) + .unwrap(); + guard.gshadow_file.flush().unwrap(); + } +} + +/// groupmod执行器 +pub struct GModExecutor; + +impl GModExecutor { + /// **执行groupmod** + /// + /// ## 参数 + /// - `info`: 组信息 + pub fn execute(info: GModInfo) { + Self::update_passwd_file(&info); + Self::update_group_file(&info); + Self::update_gshadow_file(&info); + } + + /// 更新/etc/group文件: 更新用户组信息 + fn update_group_file(info: &GModInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.group_file); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + if field[0] == info.groupname { + if let Some(new_groupname) = &info.new_groupname { + field[0] = new_groupname; + } + if let Some(new_gid) = &info.new_gid { + field[2] = new_gid; + } + } + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + + guard.group_file.set_len(0).unwrap(); + guard.group_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.group_file.write_all(new_content.as_bytes()).unwrap(); + guard.group_file.flush().unwrap(); + } + + /// 更新/etc/gshadow文件: 更新用户组密码信息 + fn update_gshadow_file(info: &GModInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.gshadow_file); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + if field[0] == info.groupname { + if let Some(new_groupname) = &info.new_groupname { + field[0] = new_groupname; + } + } + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + + guard.gshadow_file.set_len(0).unwrap(); + guard + .gshadow_file + .seek(std::io::SeekFrom::Start(0)) + .unwrap(); + guard + .gshadow_file + .write_all(new_content.as_bytes()) + .unwrap(); + guard.gshadow_file.flush().unwrap(); + } + + /// 更新/etc/passwd文件: 更新用户组ID信息,因为用户组ID可能会被修改 + fn update_passwd_file(info: &GModInfo) { + let mut new_content = String::new(); + let mut guard = GLOBAL_FILE.lock().unwrap(); + let content = read_to_string(&guard.passwd_file); + for line in content.lines() { + let mut field = line.split(':').collect::>(); + if field[3] == info.gid { + if let Some(new_gid) = &info.new_gid { + field[3] = new_gid; + } + } + new_content.push_str(format!("{}\n", field.join(":")).as_str()); + } + + guard.passwd_file.set_len(0).unwrap(); + guard.passwd_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + guard.passwd_file.write_all(new_content.as_bytes()).unwrap(); + guard.passwd_file.flush().unwrap(); + } +} + +fn read_to_string(mut file: &File) -> String { + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + content +} diff --git a/user/apps/user-manage/src/executor/mod.rs b/user/apps/user-manage/src/executor/mod.rs new file mode 100644 index 000000000..f989249a2 --- /dev/null +++ b/user/apps/user-manage/src/executor/mod.rs @@ -0,0 +1,2 @@ +#![allow(dead_code)] +pub mod executor; diff --git a/user/apps/user-manage/src/lib.rs b/user/apps/user-manage/src/lib.rs new file mode 100644 index 000000000..b85c394e7 --- /dev/null +++ b/user/apps/user-manage/src/lib.rs @@ -0,0 +1,5 @@ +pub mod check; +pub mod cmd; +pub mod error; +pub mod executor; +pub mod parser; diff --git a/user/apps/user-manage/src/parser/cmd.rs b/user/apps/user-manage/src/parser/cmd.rs new file mode 100644 index 000000000..a8a8a3231 --- /dev/null +++ b/user/apps/user-manage/src/parser/cmd.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +/// 命令类型 +pub enum CmdType { + User, + Passwd, + Group, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum CmdOption { + /// 用户描述 + Comment, + /// 用户主目录 + Dir, + /// 组名 + Group, + /// 组id + Gid, + /// 终端程序 + Shell, + /// 用户id + Uid, + /// 删除用户的home目录 + Remove, + /// 添加到其它用户组中 + Append, + /// 修改用户名 + Login, + /// 设置组密码 + Passwd, + /// 修改组名 + NewGroupName, + /// 无效选项 + Invalid, +} + +impl From for CmdOption { + fn from(s: String) -> Self { + match s.as_str() { + "-c" => CmdOption::Comment, + "-d" => CmdOption::Dir, + "-G" => CmdOption::Group, + "-g" => CmdOption::Gid, + "-s" => CmdOption::Shell, + "-u" => CmdOption::Uid, + "-r" => CmdOption::Remove, + "-a" => CmdOption::Append, + "-l" => CmdOption::Login, + "-p" => CmdOption::Passwd, + "-n" => CmdOption::NewGroupName, + _ => CmdOption::Invalid, + } + } +} + +impl From for &str { + fn from(option: CmdOption) -> Self { + match option { + CmdOption::Comment => "-c", + CmdOption::Dir => "-d", + CmdOption::Group => "-G", + CmdOption::Shell => "-s", + CmdOption::Uid => "-u", + CmdOption::Login => "-l", + CmdOption::Append => "-a", + CmdOption::Gid => "-g", + CmdOption::NewGroupName => "-n", + CmdOption::Passwd => "-p", + CmdOption::Remove => "-r", + CmdOption::Invalid => "Invalid option", + } + } +} + +/// useradd/userdel/usermod命令 +#[derive(Debug)] +pub struct UserCommand { + /// 用户名 + pub username: String, + /// 选项 + pub options: HashMap, +} + +/// passwd命令 +#[derive(Debug)] +pub struct PasswdCommand { + pub username: Option, +} + +/// groupadd/groupdel/groupmod命令 +#[derive(Debug)] +pub struct GroupCommand { + pub groupname: String, + pub options: HashMap, +} diff --git a/user/apps/user-manage/src/parser/mod.rs b/user/apps/user-manage/src/parser/mod.rs new file mode 100644 index 000000000..2913a06f1 --- /dev/null +++ b/user/apps/user-manage/src/parser/mod.rs @@ -0,0 +1,3 @@ +#![allow(dead_code)] +pub mod cmd; +pub mod parser; diff --git a/user/apps/user-manage/src/parser/parser.rs b/user/apps/user-manage/src/parser/parser.rs new file mode 100644 index 000000000..5f13d4ae9 --- /dev/null +++ b/user/apps/user-manage/src/parser/parser.rs @@ -0,0 +1,137 @@ +use super::cmd::{CmdOption, GroupCommand, PasswdCommand, UserCommand}; +use crate::error::error::{ErrorHandler, ExitStatus}; +use std::collections::HashMap; + +/// 用户命令(useradd/userdel/usermod)解析器 +pub struct UserParser; + +impl UserParser { + /// **解析用户命令** + /// + /// ## 参数 + /// - `args`: 用户命令参数 + /// + /// ## 返回 + /// - `UserCommand`: 用户命令 + pub fn parse(args: Vec) -> UserCommand { + let username = args.last().unwrap().clone(); + let args = &args[1..args.len() - 1]; + let mut options = HashMap::new(); + + let mut idx = 0; + loop { + if idx >= args.len() { + break; + } + let option: CmdOption = args[idx].clone().into(); + match option { + CmdOption::Invalid => invalid_handle(), + CmdOption::Remove => { + if idx + 1 < args.len() { + let op: &str = option.clone().into(); + ErrorHandler::error_handle( + format!("Invalid arg {} of option: {}", args[idx + 1], op), + ExitStatus::InvalidCmdSyntax, + ) + } + options.insert(option, "".to_string()); + } + CmdOption::Append => { + if idx + 1 >= args.len() || idx + 2 >= args.len() || args[idx + 1] != "-G" { + ErrorHandler::error_handle( + "Invalid option: -a -G ".to_string(), + ExitStatus::InvalidCmdSyntax, + ); + } + idx += 2; + let groups = &args[idx]; + options.insert(option, groups.clone()); + } + _ => { + if idx + 1 >= args.len() { + let op: &str = option.clone().into(); + ErrorHandler::error_handle( + format!("Invalid arg of option: {}", op), + ExitStatus::InvalidCmdSyntax, + ); + } + idx += 1; + let value = args[idx].clone(); + options.insert(option, value); + } + } + idx += 1; + } + + UserCommand { username, options } + } +} + +/// passwd命令解析器 +pub struct PasswdParser; + +impl PasswdParser { + /// **解析passwd命令** + /// + /// ## 参数 + /// - `args`: passwd命令参数 + /// + /// ## 返回 + /// - `PasswdCommand`: passwd命令 + pub fn parse(args: Vec) -> PasswdCommand { + let mut username = None; + if args.len() > 1 { + username = Some(args.last().unwrap().clone()); + } + PasswdCommand { username } + } +} + +/// 组命令(groupadd/groupdel/groupmod)解析器 +pub struct GroupParser; + +impl GroupParser { + /// **解析组命令** + /// + /// ## 参数 + /// - `args`: 组命令参数 + /// + /// ## 返回 + /// - `GroupCommand`: 组命令 + pub fn parse(args: Vec) -> GroupCommand { + let groupname = args.last().unwrap().clone(); + let args = &args[1..args.len() - 1]; + let mut options = HashMap::new(); + + let mut idx = 0; + loop { + if idx >= args.len() { + break; + } + let option: CmdOption = args[idx].clone().into(); + match option { + CmdOption::Invalid => invalid_handle(), + _ => { + if idx + 1 >= args.len() { + let op: &str = option.clone().into(); + ErrorHandler::error_handle( + format!("Invalid arg of option: {}", op), + ExitStatus::InvalidCmdSyntax, + ); + } + idx += 1; + let value = args[idx].clone(); + options.insert(option, value); + } + } + idx += 1; + } + + GroupCommand { groupname, options } + } +} + +#[inline] +fn invalid_handle() { + ErrorHandler::error_handle("Invalid option".to_string(), ExitStatus::InvalidCmdSyntax); +} diff --git a/user/dadk/config/user_manage-0.1.0.dadk b/user/dadk/config/user_manage-0.1.0.dadk new file mode 100644 index 000000000..03892a482 --- /dev/null +++ b/user/dadk/config/user_manage-0.1.0.dadk @@ -0,0 +1,24 @@ +{ + "name": "user_manage_tool", + "version": "0.1.0", + "description": "用户管理工具", + "task_type": { + "BuildFromSource": { + "Local": { + "path": "apps/user-manage" + } + } + }, + "depends": [], + "build": { + "build_command": "make install" + }, + "install": { + "in_dragonos_path": "/" + }, + "clean": { + "clean_command": "make clean" + }, + "envs": [], + "target_arch": ["x86_64"] +} \ No newline at end of file diff --git a/user/sysconfig/etc/group b/user/sysconfig/etc/group new file mode 100644 index 000000000..e69de29bb diff --git a/user/sysconfig/etc/gshadow b/user/sysconfig/etc/gshadow new file mode 100644 index 000000000..e69de29bb diff --git a/user/sysconfig/etc/passwd b/user/sysconfig/etc/passwd new file mode 100644 index 000000000..e69de29bb diff --git a/user/sysconfig/etc/shadow b/user/sysconfig/etc/shadow new file mode 100644 index 000000000..e69de29bb diff --git a/user/sysconfig/home/reach/system/shell.service b/user/sysconfig/home/reach/system/shell.service new file mode 100644 index 000000000..75bd9a405 --- /dev/null +++ b/user/sysconfig/home/reach/system/shell.service @@ -0,0 +1,8 @@ +[Unit] +Description=Shell + +[Service] +Type=simple +ExecStart=/bin/NovaShell +Restart=always +ExecStartPre=-/bin/about.elf