Skip to main content

sysknife_daemon/actions/
flatpak.rs

1use super::{command_mechanism, ActionMechanism, ActionSpec};
2use sysknife_types::RiskLevel;
3
4pub fn specs() -> Vec<ActionSpec> {
5    vec![
6        install_flatpak("testuser", "app-id", "flathub"),
7        remove_flatpak("testuser", "app-id"),
8        search_flatpak_apps("search-term"),
9        list_flatpak_remotes("testuser"),
10        list_installed_flatpaks("testuser"),
11        add_flatpak_remote("testuser", "remote", "https://example.invalid"),
12        remove_flatpak_remote("testuser", "remote"),
13        get_flatpak_app_info("testuser", "app-id"),
14        update_flatpak("testuser", Some("com.example.App")),
15        ubuntu_install_flatpak("testuser", "app-id", "flathub"),
16        ubuntu_remove_flatpak("testuser", "app-id"),
17        ubuntu_update_flatpak("testuser", Some("com.example.App")),
18        ubuntu_list_flatpaks("testuser"),
19    ]
20}
21
22/// Run a Flatpak command as the target user via `sudo runuser -u user -- flatpak <argv>`.
23///
24/// Flatpak user installations live under `~/.local/share/flatpak/` and are
25/// accessed through the user's D-Bus session. The daemon runs as `sysknife`
26/// (a system user) with no user installation; `runuser -u` switches to the
27/// correct user UID without spawning a login shell, so each argv element is
28/// passed to `flatpak` verbatim.
29///
30/// **Shell-injection safety:** unlike `runuser -l user -c "<shell-string>"`,
31/// the `-u user -- argv` form bypasses the shell entirely. There is no string
32/// interpolation, no metacharacter expansion, and no quoting concern — every
33/// argument reaches `flatpak(1)` exactly as supplied. Callers must still pass
34/// arguments through `validated_safe_arg`/`validated_username` upstream so a
35/// hostile value cannot impersonate a flag (`-X`) or break out of the
36/// command's own option parser, but they no longer have to defend against
37/// shell metacharacters.
38fn flatpak_as(username: &str, args: &[&str]) -> ActionMechanism {
39    let mut argv: Vec<String> = vec![
40        "runuser".to_string(),
41        "-u".to_string(),
42        username.to_string(),
43        "--".to_string(),
44        "flatpak".to_string(),
45    ];
46    argv.extend(args.iter().map(|s| s.to_string()));
47    ActionMechanism::Command {
48        program: "sudo",
49        args: argv,
50    }
51}
52
53pub fn install_flatpak(username: &str, app_id: &str, remote: &str) -> ActionSpec {
54    ActionSpec {
55        action_name: "InstallFlatpak",
56        mechanism: flatpak_as(username, &["install", "--user", "-y", remote, app_id]),
57        risk_level: RiskLevel::Medium,
58        reboot_required: false,
59        rollback_available: false,
60    }
61}
62
63pub fn remove_flatpak(username: &str, app_id: &str) -> ActionSpec {
64    ActionSpec {
65        action_name: "RemoveFlatpak",
66        mechanism: flatpak_as(username, &["uninstall", "--user", "-y", app_id]),
67        risk_level: RiskLevel::Medium,
68        reboot_required: false,
69        rollback_available: false,
70    }
71}
72
73/// Search is system-wide (no user context needed) — the Flatpak repo index
74/// is shared and does not require a D-Bus session or user installation.
75pub fn search_flatpak_apps(term: &str) -> ActionSpec {
76    ActionSpec {
77        action_name: "SearchFlatpakApps",
78        mechanism: command_mechanism("flatpak", ["search", term]),
79        risk_level: RiskLevel::Low,
80        reboot_required: false,
81        rollback_available: false,
82    }
83}
84
85pub fn list_flatpak_remotes(username: &str) -> ActionSpec {
86    ActionSpec {
87        action_name: "ListFlatpakRemotes",
88        mechanism: flatpak_as(username, &["remotes", "--user", "--columns=name,url"]),
89        risk_level: RiskLevel::Low,
90        reboot_required: false,
91        rollback_available: false,
92    }
93}
94
95pub fn add_flatpak_remote(username: &str, remote: &str, url: &str) -> ActionSpec {
96    ActionSpec {
97        action_name: "AddFlatpakRemote",
98        mechanism: flatpak_as(
99            username,
100            &["remote-add", "--user", "--if-not-exists", remote, url],
101        ),
102        risk_level: RiskLevel::Medium,
103        reboot_required: false,
104        rollback_available: false,
105    }
106}
107
108pub fn remove_flatpak_remote(username: &str, remote: &str) -> ActionSpec {
109    ActionSpec {
110        action_name: "RemoveFlatpakRemote",
111        mechanism: flatpak_as(username, &["remote-delete", "--user", remote]),
112        risk_level: RiskLevel::Medium,
113        reboot_required: false,
114        rollback_available: false,
115    }
116}
117
118pub fn list_installed_flatpaks(username: &str) -> ActionSpec {
119    ActionSpec {
120        action_name: "ListInstalledFlatpaks",
121        mechanism: flatpak_as(
122            username,
123            &[
124                "list",
125                "--user",
126                "--app",
127                "--columns=application,name,version,origin",
128            ],
129        ),
130        risk_level: RiskLevel::Low,
131        reboot_required: false,
132        rollback_available: false,
133    }
134}
135
136pub fn update_flatpak(username: &str, app_id: Option<&str>) -> ActionSpec {
137    let mechanism = match app_id {
138        Some(id) => flatpak_as(username, &["update", "--user", "-y", id]),
139        None => flatpak_as(username, &["update", "--user", "-y"]),
140    };
141    ActionSpec {
142        action_name: "UpdateFlatpak",
143        mechanism,
144        risk_level: RiskLevel::Medium,
145        reboot_required: false,
146        rollback_available: false,
147    }
148}
149
150pub fn get_flatpak_app_info(username: &str, app_id: &str) -> ActionSpec {
151    ActionSpec {
152        action_name: "GetFlatpakAppInfo",
153        mechanism: flatpak_as(username, &["info", "--user", app_id]),
154        risk_level: RiskLevel::Low,
155        reboot_required: false,
156        rollback_available: false,
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Ubuntu-specific Flatpak actions
162//
163// On Ubuntu, Flatpak is not installed by default (unlike Fedora Atomic where
164// it ships with the base image). These Ubuntu-routed actions use the same
165// argv construction as their Fedora counterparts — `flatpak_as` is reused
166// verbatim — but carry distinct action names so the daemon's policy layer
167// and the brain's routing can treat them separately. No code is duplicated:
168// every Ubuntu wrapper delegates directly to the shared `flatpak_as` helper.
169// ---------------------------------------------------------------------------
170
171/// Install a Flatpak app on Ubuntu (`sudo runuser -u <user> -- flatpak install --user -y <remote> <app>`).
172///
173/// Identical argv to `InstallFlatpak` on Fedora. Distinct action name for
174/// Ubuntu-specific routing in the daemon and LLM prompt.
175///
176/// Risk: Medium. Installs a sandboxed application from a Flatpak remote.
177pub fn ubuntu_install_flatpak(username: &str, app_id: &str, remote: &str) -> ActionSpec {
178    ActionSpec {
179        action_name: "UbuntuInstallFlatpak",
180        mechanism: flatpak_as(username, &["install", "--user", "-y", remote, app_id]),
181        risk_level: RiskLevel::Medium,
182        reboot_required: false,
183        rollback_available: false,
184    }
185}
186
187/// Remove a Flatpak app on Ubuntu (`sudo runuser -u <user> -- flatpak uninstall --user -y <app>`).
188///
189/// Risk: Medium. Uninstalls a sandboxed Flatpak application.
190pub fn ubuntu_remove_flatpak(username: &str, app_id: &str) -> ActionSpec {
191    ActionSpec {
192        action_name: "UbuntuRemoveFlatpak",
193        mechanism: flatpak_as(username, &["uninstall", "--user", "-y", app_id]),
194        risk_level: RiskLevel::Medium,
195        reboot_required: false,
196        rollback_available: false,
197    }
198}
199
200/// Update Flatpak app(s) on Ubuntu.
201///
202/// When `app_id` is `Some`, updates only that application; when `None`, updates
203/// all installed Flatpak apps for the user.
204///
205/// Risk: Medium. May pull new versions of installed apps.
206pub fn ubuntu_update_flatpak(username: &str, app_id: Option<&str>) -> ActionSpec {
207    let mechanism = match app_id {
208        Some(id) => flatpak_as(username, &["update", "--user", "-y", id]),
209        None => flatpak_as(username, &["update", "--user", "-y"]),
210    };
211    ActionSpec {
212        action_name: "UbuntuUpdateFlatpak",
213        mechanism,
214        risk_level: RiskLevel::Medium,
215        reboot_required: false,
216        rollback_available: false,
217    }
218}
219
220/// List installed Flatpak apps on Ubuntu.
221///
222/// Risk: Low. Read-only enumeration of the user's Flatpak installation.
223pub fn ubuntu_list_flatpaks(username: &str) -> ActionSpec {
224    ActionSpec {
225        action_name: "UbuntuListFlatpaks",
226        mechanism: flatpak_as(
227            username,
228            &[
229                "list",
230                "--user",
231                "--app",
232                "--columns=application,name,version,origin",
233            ],
234        ),
235        risk_level: RiskLevel::Low,
236        reboot_required: false,
237        rollback_available: false,
238    }
239}