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        // Through the helper: shadow-utils honours `userdel -R <dir>` even
79        // after `--`, so no sudoers spelling of `userdel` could refuse it.
80        mechanism: command_mechanism(
81            "sudo",
82            ["/usr/lib/sysknife/action-steps", "user-delete", username],
83        ),
84        // High risk: permanently removes login access — same class as SSH key
85        // removal and group membership changes.
86        risk_level: RiskLevel::High,
87        reboot_required: false,
88        rollback_available: false,
89    }
90}
91
92pub fn add_user_to_group(username: &str, group: &str) -> ActionSpec {
93    // On Fedora Atomic, system groups (wheel, adm, systemd-journal, etc.) live
94    // in the read-only /usr/lib/group OSTree layer. `usermod` only modifies
95    // /etc/group. If the group is absent from /etc/group, usermod silently
96    // succeeds without actually adding the user. Fix: copy the entry via
97    // `getent group` (which merges /usr/lib/group + /etc/group) if missing.
98    ActionSpec {
99        action_name: "AddUserToGroup",
100        mechanism: command_mechanism(
101            "sudo",
102            [
103                "/usr/lib/sysknife/action-steps",
104                "group-add",
105                username,
106                group,
107            ],
108        ),
109        // High risk: adding a user to a privileged group (e.g. `wheel`) grants
110        // sudo / sysknife-admin rights, constituting a privilege escalation if
111        // performed at lower than Admin level.
112        risk_level: RiskLevel::High,
113        reboot_required: false,
114        rollback_available: false,
115    }
116}
117
118pub fn remove_user_from_group(username: &str, group: &str) -> ActionSpec {
119    // Same Fedora Atomic group-layer issue as AddUserToGroup: `gpasswd` fails
120    // with "group does not exist in /etc/group" for system groups. Ensure the
121    // entry is present in /etc/group before deletion.
122    ActionSpec {
123        action_name: "RemoveUserFromGroup",
124        mechanism: command_mechanism(
125            "sudo",
126            [
127                "/usr/lib/sysknife/action-steps",
128                "group-remove",
129                username,
130                group,
131            ],
132        ),
133        // High risk: mirrors AddUserToGroup — removing from a privileged group
134        // is equally impactful and should require the same Admin authorization.
135        risk_level: RiskLevel::High,
136        reboot_required: false,
137        rollback_available: false,
138    }
139}
140
141/// Create a group (`sudo action-steps group-create [--system] <group>`, which
142/// runs `groupadd [--system] <group>`).
143///
144/// High risk: groups gate access to privileged resources, so creating one is a
145/// privilege-relevant operation on par with the rest of the user/group family.
146/// `system` selects a system group (GID from the system range) via `--system`.
147pub fn create_group(group: &str, system: bool) -> ActionSpec {
148    let mut args = vec![
149        "/usr/lib/sysknife/action-steps".to_string(),
150        "group-create".to_string(),
151    ];
152    if system {
153        args.push("--system".to_string());
154    }
155    args.push(group.to_string());
156    ActionSpec {
157        action_name: "CreateGroup",
158        mechanism: super::ActionMechanism::Command {
159            program: "sudo",
160            args,
161        },
162        risk_level: RiskLevel::Medium,
163        reboot_required: false,
164        rollback_available: false,
165    }
166}
167
168/// Delete a group (`sudo action-steps group-delete <group>`, which runs
169/// `groupdel <group>`).
170///
171/// High risk: irreversible; removing a group can strip access from every member
172/// and orphan file ownership. `groupdel` itself refuses to remove a user's
173/// primary group, which is a useful built-in guard.
174pub fn delete_group(group: &str) -> ActionSpec {
175    ActionSpec {
176        action_name: "DeleteGroup",
177        mechanism: command_mechanism(
178            "sudo",
179            ["/usr/lib/sysknife/action-steps", "group-delete", group],
180        ),
181        risk_level: RiskLevel::High,
182        reboot_required: false,
183        rollback_available: false,
184    }
185}
186
187/// Lock a user account (`sudo usermod --lock <username>`).
188///
189/// High risk: disables password login for the account without deleting it
190/// (reversible via [`unlock_user_account`]). Note this does not terminate the
191/// user's existing sessions or disable SSH-key logins.
192pub fn lock_user_account(username: &str) -> ActionSpec {
193    ActionSpec {
194        action_name: "LockUserAccount",
195        mechanism: command_mechanism(
196            "sudo",
197            ["/usr/lib/sysknife/action-steps", "user-lock", username],
198        ),
199        risk_level: RiskLevel::High,
200        reboot_required: false,
201        rollback_available: false,
202    }
203}
204
205/// Unlock a previously locked user account (`sudo usermod --unlock <username>`).
206///
207/// High risk: re-enables password login — an access-control change on par with
208/// the rest of the user/group family.
209pub fn unlock_user_account(username: &str) -> ActionSpec {
210    ActionSpec {
211        action_name: "UnlockUserAccount",
212        mechanism: command_mechanism(
213            "sudo",
214            ["/usr/lib/sysknife/action-steps", "user-unlock", username],
215        ),
216        risk_level: RiskLevel::High,
217        reboot_required: false,
218        rollback_available: false,
219    }
220}