Skip to main content

sysknife_daemon/actions/
users.rs

1use super::{command_mechanism, ActionSpec};
2use sysknife_types::RiskLevel;
3
4pub fn specs() -> Vec<ActionSpec> {
5    vec![
6        list_users(),
7        list_groups(),
8        create_user("alice", Some("/bin/bash"), Some("/home/alice")),
9        delete_user("alice"),
10        add_user_to_group("alice", "wheel"),
11        remove_user_from_group("alice", "wheel"),
12        create_group("developers", false),
13        delete_group("developers"),
14        lock_user_account("alice"),
15        unlock_user_account("alice"),
16    ]
17}
18
19pub fn list_users() -> ActionSpec {
20    ActionSpec {
21        action_name: "ListUsers",
22        mechanism: command_mechanism("getent", ["passwd"]),
23        risk_level: RiskLevel::Low,
24        reboot_required: false,
25        rollback_available: false,
26    }
27}
28
29pub fn list_groups() -> ActionSpec {
30    ActionSpec {
31        action_name: "ListGroups",
32        mechanism: command_mechanism("getent", ["group"]),
33        risk_level: RiskLevel::Low,
34        reboot_required: false,
35        rollback_available: false,
36    }
37}
38
39pub fn create_user(username: &str, shell: Option<&str>, home: Option<&str>) -> ActionSpec {
40    let mut args = vec!["useradd".to_string(), "--create-home".to_string()];
41    if let Some(home) = home {
42        args.push("--home-dir".to_string());
43        args.push(home.to_string());
44    }
45    if let Some(shell) = shell {
46        args.push("--shell".to_string());
47        args.push(shell.to_string());
48    }
49    args.push(username.to_string());
50
51    ActionSpec {
52        action_name: "CreateUser",
53        mechanism: super::ActionMechanism::Command {
54            program: "sudo",
55            args,
56        },
57        risk_level: RiskLevel::High,
58        reboot_required: false,
59        rollback_available: false,
60    }
61}
62
63pub fn delete_user(username: &str) -> ActionSpec {
64    ActionSpec {
65        action_name: "DeleteUser",
66        mechanism: command_mechanism("sudo", ["userdel", username]),
67        // High risk: permanently removes login access — same class as SSH key
68        // removal and group membership changes.
69        risk_level: RiskLevel::High,
70        reboot_required: false,
71        rollback_available: false,
72    }
73}
74
75pub fn add_user_to_group(username: &str, group: &str) -> ActionSpec {
76    // On Fedora Atomic, system groups (wheel, adm, systemd-journal, etc.) live
77    // in the read-only /usr/lib/group OSTree layer. `usermod` only modifies
78    // /etc/group. If the group is absent from /etc/group, usermod silently
79    // succeeds without actually adding the user. Fix: copy the entry via
80    // `getent group` (which merges /usr/lib/group + /etc/group) if missing.
81    let script = format!(
82        "grep -q '^{}:' /etc/group || getent group '{}' >> /etc/group; usermod --append --groups '{}' '{}'",
83        group, group, group, username
84    );
85    ActionSpec {
86        action_name: "AddUserToGroup",
87        mechanism: command_mechanism("sudo", ["sh", "-c", script.as_str()]),
88        // High risk: adding a user to a privileged group (e.g. `wheel`) grants
89        // sudo / sysknife-admin rights, constituting a privilege escalation if
90        // performed at lower than Admin level.
91        risk_level: RiskLevel::High,
92        reboot_required: false,
93        rollback_available: false,
94    }
95}
96
97pub fn remove_user_from_group(username: &str, group: &str) -> ActionSpec {
98    // Same Fedora Atomic group-layer issue as AddUserToGroup: `gpasswd` fails
99    // with "group does not exist in /etc/group" for system groups. Ensure the
100    // entry is present in /etc/group before deletion.
101    let script = format!(
102        "grep -q '^{}:' /etc/group || getent group '{}' >> /etc/group; gpasswd --delete '{}' '{}'",
103        group, group, username, group
104    );
105    ActionSpec {
106        action_name: "RemoveUserFromGroup",
107        mechanism: command_mechanism("sudo", ["sh", "-c", script.as_str()]),
108        // High risk: mirrors AddUserToGroup — removing from a privileged group
109        // is equally impactful and should require the same Admin authorization.
110        risk_level: RiskLevel::High,
111        reboot_required: false,
112        rollback_available: false,
113    }
114}
115
116/// Create a group (`sudo groupadd [--system] <group>`).
117///
118/// High risk: groups gate access to privileged resources, so creating one is a
119/// privilege-relevant operation on par with the rest of the user/group family.
120/// `system` selects a system group (GID from the system range) via `--system`.
121pub fn create_group(group: &str, system: bool) -> ActionSpec {
122    let mut args = vec!["groupadd".to_string()];
123    if system {
124        args.push("--system".to_string());
125    }
126    args.push(group.to_string());
127    ActionSpec {
128        action_name: "CreateGroup",
129        mechanism: super::ActionMechanism::Command {
130            program: "sudo",
131            args,
132        },
133        risk_level: RiskLevel::Medium,
134        reboot_required: false,
135        rollback_available: false,
136    }
137}
138
139/// Delete a group (`sudo groupdel <group>`).
140///
141/// High risk: irreversible; removing a group can strip access from every member
142/// and orphan file ownership. `groupdel` itself refuses to remove a user's
143/// primary group, which is a useful built-in guard.
144pub fn delete_group(group: &str) -> ActionSpec {
145    ActionSpec {
146        action_name: "DeleteGroup",
147        mechanism: command_mechanism("sudo", ["groupdel", group]),
148        risk_level: RiskLevel::High,
149        reboot_required: false,
150        rollback_available: false,
151    }
152}
153
154/// Lock a user account (`sudo usermod --lock <username>`).
155///
156/// High risk: disables password login for the account without deleting it
157/// (reversible via [`unlock_user_account`]). Note this does not terminate the
158/// user's existing sessions or disable SSH-key logins.
159pub fn lock_user_account(username: &str) -> ActionSpec {
160    ActionSpec {
161        action_name: "LockUserAccount",
162        mechanism: command_mechanism("sudo", ["usermod", "--lock", username]),
163        risk_level: RiskLevel::High,
164        reboot_required: false,
165        rollback_available: false,
166    }
167}
168
169/// Unlock a previously locked user account (`sudo usermod --unlock <username>`).
170///
171/// High risk: re-enables password login — an access-control change on par with
172/// the rest of the user/group family.
173pub fn unlock_user_account(username: &str) -> ActionSpec {
174    ActionSpec {
175        action_name: "UnlockUserAccount",
176        mechanism: command_mechanism("sudo", ["usermod", "--unlock", username]),
177        risk_level: RiskLevel::High,
178        reboot_required: false,
179        rollback_available: false,
180    }
181}