Skip to main content

ui/
perm.rs

1// perm -- what build_rs.py builds into an app, set up before it starts, and the app's
2// permissions on the UI side (plat::policy, docs/PERMISSIONS.md, docs/VFS.md)
3//
4//   boot(app, manifest, assets)   (main!) the app's name for its folders (plat::vfs::
5//                           set_app_id), its asset bundle as app:// (JFA1, apps/NAME/assets),
6//                           then its manifest (apps/NAME/PERMISSIONS, a BINS program's own
7//                           PERMISSIONS.<bin>); Err(exit code) for a bundle or manifest it
8//                           refuses.  Before the manifest: the user's languages and format
9//                           (i18n::boot: jfgui_LANG, else the system in window mode, German in
10//                           headless runs).  The paths on the command line come from the user:
11//                           with fs.user they may be read and written (folders with everything
12//                           below).  The steps are plat::boot's, which console programs
13//                           (rt::main!) run without the UI.  Script runs (--script) keep the
14//                           answers to permission questions in memory (jfgui_PERM_STORE or
15//                           `perm store` for a file).
16//   grant_user(path)        the same for a path of the file dialog or a drop
17//   trusted(f)              f on this thread without file checks: the library's own file
18//                           dialog lists folders, scripts read and write their files
19//   ask_here(on)            (runtime) the UI thread asks the questions of ask lines and agent
20//                           actions (window runs; scripts after `perm answer dialog`)
21//   answer(id, a) / set_answer(a) / set_wait(ms) / set_store(path)   (scripts: perm ...)
22//   Ui::perm_dialog()       (run_passes, after App::frame) the first open question as a content
23//                           dialog: what, the path / host / program / action, the plugin, the
24//                           manifest's reason (through the app's catalogue), the ask line, and
25//                           "Einmal erlauben" (until the program ends), "Immer erlauben"
26//                           (stored), "Ablehnen" (until the program ends), "Nie erlauben"
27//                           (stored); Esc = Ablehnen.  The answer reaches the app as
28//                           UIC_PERMISSION (arg 1 allowed, 0 refused): an access the UI thread
29//                           made while the question was open was refused, it may try again.
30//   Ui::permissions_page(id) -> bool   the ready-made settings section: every ask line and
31//                           answered agent action with what it allows, the rule, the reason
32//                           and the answer; "Zurücksetzen: <rule>" revokes one (asked again
33//                           on the next access), "Alle zurücksetzen" all; true when an answer
34//                           was revoked.  Ids: the scope of `id`, then the rule ("reset").
35//
36// The key of plat::policy stays here: apps cannot widen their own permissions or answer
37// their own questions.
38
39use alloc::format;
40use alloc::string::String;
41use core::sync::atomic::{AtomicBool, Ordering};
42
43use plat::policy::{Answer, Key};
44use rt::sync::SpinLock;
45
46use super::consts::*;
47use super::i18n::{fill, t_text, tr};
48use super::keys::IC_UNDO;
49use super::win::Shared;
50use super::Ui;
51
52static KEY: SpinLock<Option<Key>> = SpinLock::new(None);
53// the UI thread asks (ask_here)
54static ASKING: AtomicBool = AtomicBool::new(false);
55
56const DLG: u32 = 0xFFFF_7700;
57const B_ONCE: u32 = 0xFFFF_7701;
58const B_ALWAYS: u32 = 0xFFFF_7702;
59const B_DENY: u32 = 0xFFFF_7703;
60const B_NEVER: u32 = 0xFFFF_7704;
61
62/// the question on screen
63#[derive(Default)]
64pub(crate) struct S {
65    open: Shared<i32>,
66    shown: u32,
67}
68
69/// (main!) set up the app: its name, its assets (app://), its permissions
70#[doc(hidden)]
71pub fn boot(app: Option<&str>, manifest: Option<&str>, assets: &'static [u8]) -> Result<(), i32> {
72    plat::boot::name_and_assets(app, assets)?;
73    // the user's languages and format (headless runs: jfgui_LANG or German)
74    let headless = ["--shot", "--script"].iter().any(|o| plat::arg_value(o).is_some())
75        || plat::args().iter().any(|a| a == "--agent-json" || a == "--agent-mcp");
76    crate::i18n::boot(headless);
77    // the app's manifest (None: the app has none, everything stays allowed; the key still
78    // answers the questions of agent actions)
79    let key = plat::boot::permissions(manifest)?;
80    // test scripts never read or write the user's stored answers
81    if plat::arg_value("--script").is_some() && plat::env("jfgui_PERM_STORE").is_none() {
82        if let Some(k) = &key {
83            k.set_store(None);
84        }
85    }
86    *KEY.lock() = key;
87    Ok(())
88}
89
90fn key() -> Option<Key> {
91    KEY.lock().clone()
92}
93
94/// a path the user chose (file dialog, drop): read and write it (with fs.user)
95pub(crate) fn grant_user(path: &str) {
96    if let Some(k) = key() {
97        plat::boot::grant_user(&k, path);
98    }
99}
100
101/// f without the file checks of the policy on this thread
102pub(crate) fn trusted<R>(f: impl FnOnce() -> R) -> R {
103    match key() {
104        Some(k) => k.trusted(f),
105        None => f(),
106    }
107}
108
109/// the paths of a drop list (each one the user's)
110pub(crate) fn grant_all(list: &[String]) {
111    for p in list {
112        grant_user(p);
113    }
114}
115
116/// (runtime, scripts) this thread shows the permission questions (on) or nobody does
117pub(crate) fn ask_here(on: bool) {
118    if let Some(k) = key() {
119        k.ask_here(on);
120        ASKING.store(on, Ordering::Release);
121    }
122}
123
124/// whether this run shows the questions (perm_dialog)
125pub(crate) fn asking() -> bool {
126    ASKING.load(Ordering::Acquire)
127}
128
129/// (scripts) answer question id
130pub(crate) fn answer(id: u32, a: Answer) -> bool {
131    key().is_some_and(|k| k.answer(id, a))
132}
133
134/// (scripts) answer every question with a without asking (None: ask)
135pub(crate) fn set_answer(a: Option<Answer>) {
136    if let Some(k) = key() {
137        k.set_answer(a);
138    }
139}
140
141/// (scripts) how long other threads wait for an answer (ms)
142pub(crate) fn set_wait(ms: u32) {
143    if let Some(k) = key() {
144        k.set_wait(ms);
145    }
146}
147
148/// (scripts) where Always / Never are kept (None: memory only)
149pub(crate) fn set_store(path: Option<&str>) {
150    if let Some(k) = key() {
151        k.set_store(path);
152    }
153}
154
155// what a kind of access allows, as the dialog and the page say it
156fn what(kind: &str) -> &'static str {
157    match kind {
158        "fs.read" => tr("Dateien lesen"),
159        "fs.write" => tr("Dateien schreiben"),
160        "process" => tr("Ein Programm starten"),
161        "url" => tr("Eine Adresse öffnen"),
162        "plugins" => tr("Ein Plugin laden"),
163        "print" => tr("Drucken"),
164        "secrets" => tr("Gespeicherte Kennwörter verwenden"),
165        "autostart" => tr("Beim Anmelden starten"),
166        "audio.output" => tr("Klänge abspielen"),
167        "net.connect" => tr("Eine Netzwerkverbindung aufbauen"),
168        "net.listen" => tr("Netzwerkverbindungen annehmen"),
169        "agent" => tr("Eine Aktion für den Agenten ausführen"),
170        "location" => tr("Den Standort abfragen"),
171        "sensors" => tr("Die Bewegungssensoren lesen"),
172        _ => tr("Zugriff erlauben?"),
173    }
174}
175
176// the state of an answer on the page
177fn state(a: Option<Answer>) -> &'static str {
178    match a {
179        None => tr("Wird beim nächsten Zugriff gefragt"),
180        Some(Answer::Once) => tr("Bis zum Programmende erlaubt"),
181        Some(Answer::Always) => tr("Immer erlaubt"),
182        Some(Answer::Deny) => tr("Bis zum Programmende abgelehnt"),
183        Some(Answer::Never) => tr("Nie erlaubt"),
184    }
185}
186
187impl Ui {
188    /// (run_passes) the first open permission question as a content dialog (module header)
189    pub(crate) fn perm_dialog(&mut self) {
190        if !asking() {
191            return;
192        }
193        let qs = plat::policy::questions();
194        let Some(q) = qs.first() else {
195            self.m.perm.shown = 0;
196            return;
197        };
198        if self.m.perm.shown != q.id {
199            self.m.perm.shown = q.id;
200            self.m.perm.open.set(1);
201        }
202        let open = self.m.perm.open.clone();
203        let mut picked = None;
204        if self.dialog_begin(DLG, tr("Zugriff erlauben?"), &open) {
205            self.text(TS_BODY_STRONG, what(q.kind));
206            if !q.target.is_empty() {
207                self.text(TS_BODY, &q.target);
208            }
209            if !q.plugin.is_empty() {
210                self.text(TS_BODY, &fill(tr("Für das Plugin: {}"), &[&q.plugin]));
211            }
212            if !q.reason.is_empty() {
213                self.text(TS_BODY, &fill(tr("Grund: {}"), &[&t_text(&q.reason)]));
214            }
215            self.text(TS_CAPTION, &fill(tr("Regel: {}"), &[&q.rule]));
216            self.box_begin(0, BOX_H | BOX_WRAP, 8);
217            if self.accent_button(B_ONCE, tr("Einmal erlauben")) & EV_ACT != 0 {
218                picked = Some(Answer::Once);
219            }
220            if self.button(B_ALWAYS, tr("Immer erlauben")) & EV_ACT != 0 {
221                picked = Some(Answer::Always);
222            }
223            if self.button(B_DENY, tr("Ablehnen")) & EV_ACT != 0 {
224                picked = Some(Answer::Deny);
225            }
226            if self.button(B_NEVER, tr("Nie erlauben")) & EV_ACT != 0 {
227                picked = Some(Answer::Never);
228            }
229            self.box_end();
230        }
231        self.dialog_end();
232        if picked.is_none() && open.get() == 0 {
233            picked = Some(Answer::Deny); // Esc
234        }
235        if let Some(a) = picked {
236            open.set(0);
237            self.m.perm.shown = 0;
238            if answer(q.id, a) {
239                self.cmd_run(UIC_PERMISSION, a.allows() as u32);
240            }
241            self.repass();
242        }
243    }
244
245    /// the permissions section of a settings page: the answers to the app's permission
246    /// questions, each with a reset (module header) -> an answer was revoked
247    #[cfg_attr(jfgui_dev, track_caller)]
248    pub fn permissions_page(&mut self, id: u32) -> bool {
249        let _src = self.src_scope();
250        let list = plat::policy::decisions();
251        let mut changed = false;
252        self.id_push(id);
253        self.box_begin(0, BOX_V | BOX_CARD | pad(16), 12);
254        if list.is_empty() {
255            self.text(TS_BODY, tr("Keine Rückfragen"));
256        }
257        for d in &list {
258            self.id_push_str(&d.rule);
259            self.box_begin(0, BOX_H | BOX_XCENTER, 12);
260            self.next_size(-1, 0);
261            self.box_begin(0, BOX_V, 2);
262            self.text(TS_BODY_STRONG, what(d.kind));
263            let target = d.rule.split_once(' ').map_or("", |x| x.1);
264            if !target.is_empty() {
265                self.text(TS_BODY, target);
266            }
267            if !d.reason.is_empty() {
268                self.text(TS_CAPTION, &t_text(&d.reason));
269            }
270            self.text(TS_CAPTION, state(d.answer));
271            self.box_end();
272            if d.answer.is_none() {
273                self.next_flags(ST_DISABLED);
274            }
275            let reset = format!("{}: {}", tr("Zurücksetzen"), d.rule);
276            if self.icon_button(self.id("reset"), IC_UNDO, &reset) & EV_ACT != 0 {
277                changed |= plat::policy::forget(&d.rule);
278            }
279            self.box_end();
280            self.id_pop();
281        }
282        if !list.iter().any(|d| d.answer.is_some()) {
283            self.next_flags(ST_DISABLED);
284        }
285        if self.button(self.id("reset.all"), tr("Alle zurücksetzen")) & EV_ACT != 0 {
286            plat::policy::forget_all();
287            changed = true;
288        }
289        self.box_end();
290        self.id_pop();
291        if changed {
292            self.repass();
293        }
294        changed
295    }
296}