feat: add permission consent prompts, drop permitted capability mask
This commit is contained in:
1 parent
c54181103f
commit
ae5e84592d
25 files changed
+1632
-94
No files matched your search
@@ -8,6 +8,7 @@
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/capabilities.h>
|
||||
#include <montauk/consent.h>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
@@ -30,6 +31,7 @@ static constexpr int GRID_ROW_H = 22;
|
||||
static constexpr int GRID_COL_W = 86;
|
||||
static constexpr int MAX_GRANTS = 64;
|
||||
static constexpr int MAX_ISSUES = 4;
|
||||
static constexpr int MAX_DECISIONS = 64;
|
||||
|
||||
static constexpr int CAP_COUNT =
|
||||
(int)(sizeof(montauk::caps::NAMES) / sizeof(montauk::caps::NAMES[0]));
|
||||
@@ -37,12 +39,14 @@ static constexpr int CAP_COUNT =
|
||||
enum Tab {
|
||||
TAB_PROGRAMS = 0,
|
||||
TAB_CAPS = 1,
|
||||
TAB_COUNT = 2,
|
||||
TAB_CONSENT = 2,
|
||||
TAB_COUNT = 3,
|
||||
};
|
||||
|
||||
static const char* const kTabLabels[TAB_COUNT] = {
|
||||
"Programs",
|
||||
"Capabilities",
|
||||
"Consent",
|
||||
};
|
||||
|
||||
// What each capability gates, in the order of montauk::caps::NAMES.
|
||||
@@ -68,16 +72,15 @@ static const char* const kCapDescriptions[CAP_COUNT] = {
|
||||
enum Field {
|
||||
FIELD_EFFECTIVE = 0,
|
||||
FIELD_DELEGABLE = 1,
|
||||
FIELD_PERMITTED = 2,
|
||||
FIELD_COUNT = 3,
|
||||
FIELD_COUNT = 2,
|
||||
};
|
||||
|
||||
static const char* const kFieldKeys[FIELD_COUNT] = {
|
||||
"effective", "delegable", "permitted",
|
||||
"effective", "delegable",
|
||||
};
|
||||
|
||||
static const char* const kFieldLabels[FIELD_COUNT] = {
|
||||
"Effective", "Delegable", "Permitted",
|
||||
"Effective", "Delegable",
|
||||
};
|
||||
|
||||
struct Grant {
|
||||
@@ -104,6 +107,11 @@ static Color g_accent = colors::ACCENT;
|
||||
static bool g_is_admin = false;
|
||||
static int g_selected_grant = 0;
|
||||
static int g_selected_cap = 0;
|
||||
static int g_selected_decision = 0;
|
||||
|
||||
// Answers given to consent prompts (0:/config/consent.toml).
|
||||
static montauk::consent::Decision g_decisions[MAX_DECISIONS];
|
||||
static int g_decision_count = 0;
|
||||
static int g_list_scroll = 0;
|
||||
static bool g_list_sb_dragging = false;
|
||||
static int g_list_sb_drag_offset = 0;
|
||||
@@ -214,14 +222,6 @@ static void load_grants() {
|
||||
|
||||
for (int i = 0; i < g_grant_count; i++) {
|
||||
Grant& g = g_grants[i];
|
||||
uint64_t* m = g.mask;
|
||||
|
||||
// Same rule as caps::lookup(): an undeclared permitted set is exactly
|
||||
// what the grant can use or pass on.
|
||||
if (!g.declared[FIELD_PERMITTED] || m[FIELD_PERMITTED] == 0)
|
||||
m[FIELD_PERMITTED] = m[FIELD_EFFECTIVE] | m[FIELD_DELEGABLE];
|
||||
else if ((m[FIELD_EFFECTIVE] | m[FIELD_DELEGABLE]) & ~m[FIELD_PERMITTED])
|
||||
add_issue(g, "Effective/delegable exceed permitted; the excess is dropped at launch");
|
||||
|
||||
if (g.path[0] == '\0') {
|
||||
add_issue(g, "No path; this grant never applies");
|
||||
@@ -231,6 +231,8 @@ static void load_grants() {
|
||||
g.binary_present = bh >= 0;
|
||||
if (bh >= 0) montauk::close(bh);
|
||||
if (!g.binary_present) add_issue(g, "Binary not found on this system");
|
||||
if (!montauk::caps::is_installed_path(g.path))
|
||||
add_issue(g, "Never applies: not under 0:/os or 0:/apps, so it runs with no capabilities");
|
||||
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (montauk::streq(g_grants[j].path, g.path)) {
|
||||
@@ -244,6 +246,25 @@ static void load_grants() {
|
||||
if (g_selected_grant >= g_grant_count) g_selected_grant = 0;
|
||||
}
|
||||
|
||||
static void load_decisions() {
|
||||
g_decision_count = montauk::consent::load_all(g_decisions, MAX_DECISIONS);
|
||||
if (g_selected_decision >= g_decision_count)
|
||||
g_selected_decision = g_decision_count > 0 ? g_decision_count - 1 : 0;
|
||||
}
|
||||
|
||||
static const char* base_name(const char* path) {
|
||||
const char* base = path;
|
||||
for (const char* p = path; *p; p++)
|
||||
if (*p == '/' || *p == ':') base = p + 1;
|
||||
return base;
|
||||
}
|
||||
|
||||
static int cap_index(uint64_t bit) {
|
||||
for (int i = 0; i < CAP_COUNT; i++)
|
||||
if (montauk::caps::NAMES[i].bit == bit) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==== Layout ====
|
||||
|
||||
static mtk::Theme app_theme() {
|
||||
@@ -271,7 +292,15 @@ static Rect list_rect() {
|
||||
}
|
||||
|
||||
static int list_count() {
|
||||
return g_tab == TAB_PROGRAMS ? g_grant_count : CAP_COUNT;
|
||||
if (g_tab == TAB_PROGRAMS) return g_grant_count;
|
||||
if (g_tab == TAB_CONSENT) return g_decision_count;
|
||||
return CAP_COUNT;
|
||||
}
|
||||
|
||||
static int* selected_for_tab() {
|
||||
if (g_tab == TAB_PROGRAMS) return &g_selected_grant;
|
||||
if (g_tab == TAB_CONSENT) return &g_selected_decision;
|
||||
return &g_selected_cap;
|
||||
}
|
||||
|
||||
static int list_visible_rows() {
|
||||
@@ -322,6 +351,15 @@ static Rect reload_button() {
|
||||
return {foot.x + foot.w - PAD - BUTTON_W, foot.y + 7, BUTTON_W, 30};
|
||||
}
|
||||
|
||||
static Rect forget_button() {
|
||||
Rect reload = reload_button();
|
||||
return {reload.x - 8 - BUTTON_W, reload.y, BUTTON_W, reload.h};
|
||||
}
|
||||
|
||||
static bool forget_visible() {
|
||||
return g_is_admin && g_tab == TAB_CONSENT && g_decision_count > 0;
|
||||
}
|
||||
|
||||
static bool mouse_in(const Rect& rect) {
|
||||
return rect.contains(g_mouse_x, g_mouse_y);
|
||||
}
|
||||
@@ -382,17 +420,24 @@ static void draw_list(Canvas& c, const mtk::Theme& theme) {
|
||||
int index = g_list_scroll + row;
|
||||
if (index >= count) break;
|
||||
Rect r = list_row_rect(row);
|
||||
bool selected = index == (g_tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap);
|
||||
bool selected = index == *selected_for_tab();
|
||||
mtk::draw_list_row(c, r, selected, false, theme);
|
||||
if (!selected && mouse_in(r))
|
||||
c.fill_rounded_rect(r.x, r.y, r.w, r.h, theme.radius_md, theme.surface_hover);
|
||||
|
||||
const char* label = g_tab == TAB_PROGRAMS
|
||||
? g_grants[index].id : montauk::caps::NAMES[index].name;
|
||||
const char* label = g_tab == TAB_PROGRAMS ? g_grants[index].id
|
||||
: g_tab == TAB_CONSENT ? base_name(g_decisions[index].path)
|
||||
: montauk::caps::NAMES[index].name;
|
||||
Color fg = selected ? theme.accent_fg : theme.text;
|
||||
int ty = r.y + (r.h - fh) / 2;
|
||||
draw_text_fit(c, r.x + 10, ty, label, r.w - 34, fg);
|
||||
|
||||
if (g_tab == TAB_CONSENT) {
|
||||
Color dot = selected ? theme.accent_fg
|
||||
: g_decisions[index].allow ? theme.accent : theme.text_muted;
|
||||
fill_circle(c, r.x + r.w - 14, r.y + r.h / 2, 4, dot);
|
||||
}
|
||||
|
||||
if (g_tab == TAB_PROGRAMS && g_grants[index].issue_count > 0) {
|
||||
Color dot = selected ? theme.accent_fg : theme.danger;
|
||||
fill_circle(c, r.x + r.w - 14, r.y + r.h / 2, 4, dot);
|
||||
@@ -459,7 +504,6 @@ static void draw_programs_pane(Canvas& c, const mtk::Theme& theme) {
|
||||
for (int f = 0; f < FIELD_COUNT; f++) {
|
||||
const char* note = nullptr;
|
||||
if (g.uses_all[f]) note = "(all)";
|
||||
else if (f == FIELD_PERMITTED && !g.declared[f]) note = "(implied)";
|
||||
if (!note) continue;
|
||||
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
|
||||
c.text(cx - text_width(note) / 2, y, note, theme.text_muted);
|
||||
@@ -468,7 +512,7 @@ static void draw_programs_pane(Canvas& c, const mtk::Theme& theme) {
|
||||
|
||||
for (int i = 0; i < CAP_COUNT; i++) {
|
||||
uint64_t bit = montauk::caps::NAMES[i].bit;
|
||||
bool any = (g.mask[FIELD_PERMITTED] & bit) != 0;
|
||||
bool any = ((g.mask[FIELD_EFFECTIVE] | g.mask[FIELD_DELEGABLE]) & bit) != 0;
|
||||
if (i % 2 == 1)
|
||||
c.fill_rect(x, y, w, GRID_ROW_H, theme.surface_alt);
|
||||
c.text(x + 6, y + (GRID_ROW_H - fh) / 2, montauk::caps::NAMES[i].name,
|
||||
@@ -541,6 +585,43 @@ static void draw_caps_pane(Canvas& c, const mtk::Theme& theme) {
|
||||
"MAY PASS IT ON (DELEGABLE)", FIELD_DELEGABLE, cap.bit, theme);
|
||||
}
|
||||
|
||||
static void draw_consent_pane(Canvas& c, const mtk::Theme& theme) {
|
||||
int x = pane_x();
|
||||
int w = pane_w();
|
||||
int fh = system_font_height();
|
||||
int y = TAB_H + PAD;
|
||||
|
||||
if (g_decision_count == 0) {
|
||||
c.text(x, y, "No consent decisions are stored.", theme.text_subtle);
|
||||
c.text(x, y + fh + 8, "Programs without a grant ask the first time they need", theme.text_muted);
|
||||
c.text(x, y + (fh + 8) * 2, "storage, network, time, display or log access.", theme.text_muted);
|
||||
return;
|
||||
}
|
||||
|
||||
const montauk::consent::Decision& d = g_decisions[g_selected_decision];
|
||||
c.text(x, y, base_name(d.path), theme.text);
|
||||
const char* state = d.allow ? "Allowed" : "Denied";
|
||||
c.text(x + w - text_width(state), y, state, d.allow ? theme.accent : theme.danger);
|
||||
y += fh + 6;
|
||||
draw_text_fit(c, x, y, d.path, w, theme.text_subtle);
|
||||
|
||||
y += fh + 14;
|
||||
mtk::draw_separator(c, x, y, w, theme);
|
||||
y += 14;
|
||||
|
||||
int ci = cap_index(d.capability);
|
||||
c.text(x, y, "CAPABILITY", theme.text_muted);
|
||||
y += fh + 6;
|
||||
c.text(x + 10, y, ci >= 0 ? montauk::caps::NAMES[ci].name : "?", theme.text);
|
||||
y += fh + 4;
|
||||
if (ci >= 0) draw_text_fit(c, x + 10, y, kCapDescriptions[ci], w - 10, theme.text_subtle);
|
||||
y += fh + 16;
|
||||
|
||||
c.text(x, y, "Forget makes the program ask again the next time it needs", theme.text_muted);
|
||||
y += fh + 4;
|
||||
c.text(x, y, "this. A copy already running keeps its access until it exits.", theme.text_muted);
|
||||
}
|
||||
|
||||
static void draw_locked(Canvas& c, const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
int y = TAB_H + 60;
|
||||
@@ -564,9 +645,13 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
||||
int flagged = 0;
|
||||
for (int i = 0; i < g_grant_count; i++)
|
||||
if (g_grants[i].issue_count > 0) flagged++;
|
||||
snprintf(summary, sizeof(summary), "%d grant%s%s, %d flagged - read-only view",
|
||||
g_grant_count, g_grant_count == 1 ? "" : "s",
|
||||
g_truncated ? " (list truncated)" : "", flagged);
|
||||
if (g_tab == TAB_CONSENT)
|
||||
snprintf(summary, sizeof(summary), "%d stored decision%s",
|
||||
g_decision_count, g_decision_count == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(summary, sizeof(summary), "%d grant%s%s, %d flagged - read-only view",
|
||||
g_grant_count, g_grant_count == 1 ? "" : "s",
|
||||
g_truncated ? " (list truncated)" : "", flagged);
|
||||
}
|
||||
c.text(PAD, foot.y + (foot.h - system_font_height()) / 2, summary, theme.text_subtle);
|
||||
|
||||
@@ -575,6 +660,11 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
||||
mtk::draw_button(c, reload, "Reload", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, mouse_in(reload), true), theme);
|
||||
}
|
||||
if (forget_visible()) {
|
||||
Rect forget = forget_button();
|
||||
mtk::draw_button(c, forget, "Forget", mtk::BUTTON_DANGER,
|
||||
mtk::widget_state(false, mouse_in(forget), true), theme);
|
||||
}
|
||||
}
|
||||
|
||||
static void render() {
|
||||
@@ -586,6 +676,7 @@ static void render() {
|
||||
mtk::draw_tab_bar(c, tab_bar_rect(), kTabLabels, TAB_COUNT, (int)g_tab, theme);
|
||||
draw_list(c, theme);
|
||||
if (g_tab == TAB_PROGRAMS) draw_programs_pane(c, theme);
|
||||
else if (g_tab == TAB_CONSENT) draw_consent_pane(c, theme);
|
||||
else draw_caps_pane(c, theme);
|
||||
} else {
|
||||
draw_locked(c, theme);
|
||||
@@ -598,8 +689,25 @@ static void render() {
|
||||
|
||||
static void reload() {
|
||||
load_grants();
|
||||
load_decisions();
|
||||
clamp_list_scroll();
|
||||
set_status("Reloaded 0:/config/capabilities.toml");
|
||||
set_status(g_tab == TAB_CONSENT ? "Reloaded 0:/config/consent.toml"
|
||||
: "Reloaded 0:/config/capabilities.toml");
|
||||
}
|
||||
|
||||
static void forget_selected() {
|
||||
if (g_decision_count == 0) return;
|
||||
const montauk::consent::Decision& d = g_decisions[g_selected_decision];
|
||||
if (montauk::consent::forget(d.id) < 0) {
|
||||
set_status("Could not write 0:/config/consent.toml");
|
||||
return;
|
||||
}
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "Forgot %s for %s", montauk::consent::name_for(d.capability),
|
||||
base_name(d.path));
|
||||
load_decisions();
|
||||
clamp_list_scroll();
|
||||
set_status(msg);
|
||||
}
|
||||
|
||||
static void select_index(int index) {
|
||||
@@ -607,8 +715,7 @@ static void select_index(int index) {
|
||||
if (count <= 0) return;
|
||||
if (index < 0) index = 0;
|
||||
if (index >= count) index = count - 1;
|
||||
if (g_tab == TAB_PROGRAMS) g_selected_grant = index;
|
||||
else g_selected_cap = index;
|
||||
*selected_for_tab() = index;
|
||||
|
||||
int visible = list_visible_rows();
|
||||
if (index < g_list_scroll) g_list_scroll = index;
|
||||
@@ -619,7 +726,7 @@ static void select_index(int index) {
|
||||
static void switch_tab(Tab tab) {
|
||||
g_tab = tab;
|
||||
g_list_scroll = 0;
|
||||
select_index(tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap);
|
||||
select_index(*selected_for_tab());
|
||||
}
|
||||
|
||||
static void handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
@@ -675,6 +782,7 @@ static void handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
}
|
||||
}
|
||||
if (reload_button().contains(mx, my)) reload();
|
||||
if (forget_visible() && forget_button().contains(mx, my)) forget_selected();
|
||||
}
|
||||
|
||||
static bool handle_key(const montauk::abi::KeyEvent& key) {
|
||||
@@ -685,10 +793,10 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
||||
}
|
||||
if (!g_is_admin) return false;
|
||||
if (key.ascii == '\t') {
|
||||
switch_tab(g_tab == TAB_PROGRAMS ? TAB_CAPS : TAB_PROGRAMS);
|
||||
switch_tab((Tab)((g_tab + 1) % TAB_COUNT));
|
||||
return true;
|
||||
}
|
||||
int current = g_tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap;
|
||||
int current = *selected_for_tab();
|
||||
if (key.scancode == 0x48) { select_index(current - 1); return true; } // Up
|
||||
if (key.scancode == 0x50) { select_index(current + 1); return true; } // Down
|
||||
if (key.ascii == 'r' || key.ascii == 'R') {
|
||||
@@ -703,7 +811,10 @@ extern "C" void _start() {
|
||||
g_accent = mtk::load_system_accent();
|
||||
|
||||
g_is_admin = session_is_admin();
|
||||
if (g_is_admin) load_grants();
|
||||
if (g_is_admin) {
|
||||
load_grants();
|
||||
load_decisions();
|
||||
}
|
||||
|
||||
if (!g_win.create("Capabilities", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
|
||||
Reference in new issue
Block a user