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
39/// Create a local account (`sudo action-steps user-create <name> [...]`).
40///
41/// Through the helper rather than `sudo useradd` directly. A sudoers grant
42/// cannot say "this subcommand and no further options": the arguments are
43/// matched as one concatenated string, so `/usr/sbin/useradd --create-home *`
44/// authorised `useradd --create-home -o -u 0 -g 0 backdoor` too. The helper
45/// re-validates the account name, the home directory and the shell, and builds
46/// the same `useradd --create-home [--home-dir H] [--shell S] <name>` argv this
47/// function used to build itself.
48pub fn create_user(username: &str, shell: Option<&str>, home: Option<&str>) -> ActionSpec {
49    let mut args = vec![
50        "/usr/lib/sysknife/action-steps".to_string(),
51        "user-create".to_string(),
52        username.to_string(),
53    ];
54    if let Some(home) = home {
55        args.push("--home-dir".to_string());
56        args.push(home.to_string());
57    }
58    if let Some(shell) = shell {
59        args.push("--shell".to_string());
60        args.push(shell.to_string());
61    }
62
63    ActionSpec {
64        action_name: "CreateUser",
65        mechanism: super::ActionMechanism::Command {
66            program: "sudo",
67            args,
68        },
69        risk_level: RiskLevel::High,
70        reboot_required: false,
71        rollback_available: false,
72    }
73}
74
75pub fn delete_user(username: &str) -> ActionSpec {
76    ActionSpec {
77        action_name: "DeleteUser",
78        mechanism: command_mechanism("sudo", ["userdel", username]),
79        // High risk: permanently removes login access — same class as SSH key
80        // removal and group membership changes.
81        risk_level: RiskLevel::High,
82        reboot_required: false,
83        rollback_available: false,
84    }
85}
86
87pub fn add_user_to_group(username: &str, group: &str) -> ActionSpec {
88    // On Fedora Atomic, system groups (wheel, adm, systemd-journal, etc.) live
89    // in the read-only /usr/lib/group OSTree layer. `usermod` only modifies
90    // /etc/group. If the group is absent from /etc/group, usermod silently
91    // succeeds without actually adding the user. Fix: copy the entry via
92    // `getent group` (which merges /usr/lib/group + /etc/group) if missing.
93    ActionSpec {
94        action_name: "AddUserToGroup",
95        mechanism: command_mechanism(
96            "sudo",
97            [
98                "/usr/lib/sysknife/action-steps",
99                "group-add",
100                username,
101                group,
102            ],
103        ),
104        // High risk: adding a user to a privileged group (e.g. `wheel`) grants
105        // sudo / sysknife-admin rights, constituting a privilege escalation if
106        // performed at lower than Admin level.
107        risk_level: RiskLevel::High,
108        reboot_required: false,
109        rollback_available: false,
110    }
111}
112
113pub fn remove_user_from_group(username: &str, group: &str) -> ActionSpec {
114    // Same Fedora Atomic group-layer issue as AddUserToGroup: `gpasswd` fails
115    // with "group does not exist in /etc/group" for system groups. Ensure the
116    // entry is present in /etc/group before deletion.
117    ActionSpec {
118        action_name: "RemoveUserFromGroup",
119        mechanism: command_mechanism(
120            "sudo",
121            [
122                "/usr/lib/sysknife/action-steps",
123                "group-remove",
124                username,
125                group,
126            ],
127        ),
128        // High risk: mirrors AddUserToGroup — removing from a privileged group
129        // is equally impactful and should require the same Admin authorization.
130        risk_level: RiskLevel::High,
131        reboot_required: false,
132        rollback_available: false,
133    }
134}
135
136/// Create a group (`sudo groupadd [--system] <group>`).
137///
138/// High risk: groups gate access to privileged resources, so creating one is a
139/// privilege-relevant operation on par with the rest of the user/group family.
140/// `system` selects a system group (GID from the system range) via `--system`.
141pub fn create_group(group: &str, system: bool) -> ActionSpec {
142    let mut args = vec!["groupadd".to_string()];
143    if system {
144        args.push("--system".to_string());
145    }
146    args.push(group.to_string());
147    ActionSpec {
148        action_name: "CreateGroup",
149        mechanism: super::ActionMechanism::Command {
150            program: "sudo",
151            args,
152        },
153        risk_level: RiskLevel::Medium,
154        reboot_required: false,
155        rollback_available: false,
156    }
157}
158
159/// Delete a group (`sudo groupdel <group>`).
160///
161/// High risk: irreversible; removing a group can strip access from every member
162/// and orphan file ownership. `groupdel` itself refuses to remove a user's
163/// primary group, which is a useful built-in guard.
164pub fn delete_group(group: &str) -> ActionSpec {
165    ActionSpec {
166        action_name: "DeleteGroup",
167        mechanism: command_mechanism("sudo", ["groupdel", group]),
168        risk_level: RiskLevel::High,
169        reboot_required: false,
170        rollback_available: false,
171    }
172}
173
174/// Lock a user account (`sudo usermod --lock <username>`).
175///
176/// High risk: disables password login for the account without deleting it
177/// (reversible via [`unlock_user_account`]). Note this does not terminate the
178/// user's existing sessions or disable SSH-key logins.
179pub fn lock_user_account(username: &str) -> ActionSpec {
180    ActionSpec {
181        action_name: "LockUserAccount",
182        mechanism: command_mechanism(
183            "sudo",
184            ["/usr/lib/sysknife/action-steps", "user-lock", username],
185        ),
186        risk_level: RiskLevel::High,
187        reboot_required: false,
188        rollback_available: false,
189    }
190}
191
192/// Unlock a previously locked user account (`sudo usermod --unlock <username>`).
193///
194/// High risk: re-enables password login — an access-control change on par with
195/// the rest of the user/group family.
196pub fn unlock_user_account(username: &str) -> ActionSpec {
197    ActionSpec {
198        action_name: "UnlockUserAccount",
199        mechanism: command_mechanism(
200            "sudo",
201            ["/usr/lib/sysknife/action-steps", "user-unlock", username],
202        ),
203        risk_level: RiskLevel::High,
204        reboot_required: false,
205        rollback_available: false,
206    }
207}