859 lines
27 KiB
C++
859 lines
27 KiB
C++
/*
|
|
* main.cpp
|
|
* MontaukOS capability grant viewer
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include <montauk/syscall.h>
|
|
#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>
|
|
|
|
extern "C" {
|
|
#include <stdio.h>
|
|
}
|
|
|
|
using namespace gui;
|
|
|
|
static constexpr int WIN_W = 760;
|
|
static constexpr int WIN_H = 582; // one grid row per capability
|
|
static constexpr int TAB_H = 36;
|
|
static constexpr int FOOTER_H = 44;
|
|
static constexpr int PAD = 18;
|
|
static constexpr int BUTTON_W = 88;
|
|
static constexpr int LIST_W = 200;
|
|
static constexpr int LIST_ROW_H = 30;
|
|
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]));
|
|
|
|
enum Tab {
|
|
TAB_PROGRAMS = 0,
|
|
TAB_CAPS = 1,
|
|
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.
|
|
static const char* const kCapDescriptions[CAP_COUNT] = {
|
|
"Inspect and terminate other processes",
|
|
"Ask the session to shut down or restart",
|
|
"Power the machine off or reset it directly",
|
|
"Put the machine to sleep (S3)",
|
|
"Format, mount and manage volumes",
|
|
"Read and write raw block devices",
|
|
"Configure network interfaces, DHCP and Wi-Fi",
|
|
"Set the system clock",
|
|
"Manage user accounts and session identities",
|
|
"Change display modes",
|
|
"Manage hardware devices (Bluetooth, radio)",
|
|
"Read the kernel log",
|
|
"Write to 0:/os and 0:/apps (never part of \"all\")",
|
|
"Read and send input to other windows (never part of \"all\")",
|
|
"Set the display backlight",
|
|
};
|
|
|
|
// ==== Grant table model ====
|
|
|
|
enum Field {
|
|
FIELD_EFFECTIVE = 0,
|
|
FIELD_DELEGABLE = 1,
|
|
FIELD_COUNT = 2,
|
|
};
|
|
|
|
static const char* const kFieldKeys[FIELD_COUNT] = {
|
|
"effective", "delegable",
|
|
};
|
|
|
|
static const char* const kFieldLabels[FIELD_COUNT] = {
|
|
"Effective", "Delegable",
|
|
};
|
|
|
|
struct Grant {
|
|
char id[32];
|
|
char path[96];
|
|
uint64_t mask[FIELD_COUNT];
|
|
bool declared[FIELD_COUNT];
|
|
bool uses_all[FIELD_COUNT];
|
|
bool binary_present;
|
|
char issues[MAX_ISSUES][96];
|
|
int issue_count;
|
|
};
|
|
|
|
static Grant g_grants[MAX_GRANTS];
|
|
static int g_grant_count = 0;
|
|
static bool g_file_present = false;
|
|
static bool g_truncated = false;
|
|
|
|
static WsWindow g_win;
|
|
static Tab g_tab = TAB_PROGRAMS;
|
|
static int g_mouse_x = -1;
|
|
static int g_mouse_y = -1;
|
|
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;
|
|
static char g_status[128] = {};
|
|
static uint64_t g_status_time = 0;
|
|
|
|
// Same test as montauk::user::is_admin(), without pulling in its BearSSL
|
|
// password-hashing dependency.
|
|
static bool session_is_admin() {
|
|
char user[64] = {};
|
|
if (montauk::getuser(user, sizeof(user)) <= 0 || !user[0]) return false;
|
|
|
|
char key[96];
|
|
snprintf(key, sizeof(key), "users.%s.role", user);
|
|
montauk::toml::Doc doc = montauk::config::load("users");
|
|
bool admin = montauk::streq(doc.get_string(key, ""), "admin");
|
|
doc.destroy();
|
|
return admin;
|
|
}
|
|
|
|
static void add_issue(Grant& g, const char* fmt, const char* a = "", const char* b = "") {
|
|
if (g.issue_count >= MAX_ISSUES) return;
|
|
snprintf(g.issues[g.issue_count++], sizeof(g.issues[0]), fmt, a, b);
|
|
}
|
|
|
|
static Grant* find_or_add_grant(const char* id, int idLen) {
|
|
for (int i = 0; i < g_grant_count; i++) {
|
|
const char* have = g_grants[i].id;
|
|
int k = 0;
|
|
while (k < idLen && have[k] == id[k]) k++;
|
|
if (k == idLen && have[k] == '\0') return &g_grants[i];
|
|
}
|
|
if (g_grant_count >= MAX_GRANTS) {
|
|
g_truncated = true;
|
|
return nullptr;
|
|
}
|
|
Grant* g = &g_grants[g_grant_count++];
|
|
montauk::memset(g, 0, sizeof(*g));
|
|
int n = idLen < (int)sizeof(g->id) - 1 ? idLen : (int)sizeof(g->id) - 1;
|
|
montauk::memcpy(g->id, id, n);
|
|
g->id[n] = '\0';
|
|
return g;
|
|
}
|
|
|
|
static void read_field(Grant& g, int field, montauk::toml::Value* v) {
|
|
g.declared[field] = true;
|
|
if (v->type != montauk::toml::Type::Array) {
|
|
add_issue(g, "'%s' is not a list of capability names", kFieldKeys[field]);
|
|
return;
|
|
}
|
|
for (int i = 0; i < v->array.count; i++) {
|
|
montauk::toml::Value* item = v->array.items[i];
|
|
if (item == nullptr || item->type != montauk::toml::Type::String) continue;
|
|
uint64_t bit = montauk::caps::bit_for_name(item->str);
|
|
if (bit == 0) {
|
|
add_issue(g, "Unknown capability \"%s\" in %s (ignored)",
|
|
item->str, kFieldKeys[field]);
|
|
continue;
|
|
}
|
|
if (montauk::streq(item->str, "all")) g.uses_all[field] = true;
|
|
g.mask[field] |= bit;
|
|
}
|
|
}
|
|
|
|
// Mirrors montauk::caps::lookup(), but keeps everything the launcher would
|
|
// silently discard so it can be shown.
|
|
static void load_grants() {
|
|
g_grant_count = 0;
|
|
g_truncated = false;
|
|
|
|
int fh = montauk::open("0:/config/capabilities.toml");
|
|
g_file_present = fh >= 0;
|
|
if (fh >= 0) montauk::close(fh);
|
|
|
|
montauk::toml::Doc doc = montauk::config::load(montauk::caps::GRANT_CONFIG);
|
|
int prefixLen = montauk::slen(montauk::caps::GRANT_PREFIX);
|
|
|
|
for (int i = 0; i < doc.entries.count; i++) {
|
|
montauk::toml::Value* entry = doc.entries.items[i];
|
|
if (entry == nullptr || entry->key == nullptr) continue;
|
|
if (!montauk::starts_with(entry->key, montauk::caps::GRANT_PREFIX)) continue;
|
|
|
|
const char* rest = entry->key + prefixLen;
|
|
int lastDot = -1;
|
|
for (int k = 0; rest[k]; k++)
|
|
if (rest[k] == '.') lastDot = k;
|
|
if (lastDot <= 0) continue;
|
|
|
|
Grant* g = find_or_add_grant(rest, lastDot);
|
|
if (g == nullptr) continue;
|
|
const char* field = rest + lastDot + 1;
|
|
|
|
if (montauk::streq(field, "path")) {
|
|
if (entry->type == montauk::toml::Type::String)
|
|
snprintf(g->path, sizeof(g->path), "%s", entry->str);
|
|
continue;
|
|
}
|
|
bool known = false;
|
|
for (int f = 0; f < FIELD_COUNT; f++) {
|
|
if (montauk::streq(field, kFieldKeys[f])) {
|
|
read_field(*g, f, entry);
|
|
known = true;
|
|
}
|
|
}
|
|
if (!known) add_issue(*g, "Unknown key '%s' (ignored)", field);
|
|
}
|
|
doc.destroy();
|
|
|
|
for (int i = 0; i < g_grant_count; i++) {
|
|
Grant& g = g_grants[i];
|
|
|
|
if (g.path[0] == '\0') {
|
|
add_issue(g, "No path; this grant never applies");
|
|
continue;
|
|
}
|
|
int bh = montauk::open(g.path);
|
|
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)) {
|
|
add_issue(g, "Shadowed by [grant.%s]; the first entry for a path wins",
|
|
g_grants[j].id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
return mtk::make_theme(g_accent);
|
|
}
|
|
|
|
static Rect tab_bar_rect() {
|
|
return {0, 0, g_win.width, TAB_H};
|
|
}
|
|
|
|
static Rect footer_rect() {
|
|
return {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
|
|
}
|
|
|
|
static constexpr int LIST_TOP_INSET = 10;
|
|
|
|
static int sidebar_w() {
|
|
return PAD + LIST_W + PAD;
|
|
}
|
|
|
|
// The whole left column, from the tab bar to the footer. The scrollbar runs
|
|
// its full height against the divider, like a pane scrollbar.
|
|
static Rect list_rect() {
|
|
return {0, TAB_H, sidebar_w(), footer_rect().y - TAB_H};
|
|
}
|
|
|
|
static int list_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() {
|
|
return (list_rect().h - LIST_TOP_INSET) / LIST_ROW_H;
|
|
}
|
|
|
|
static int list_view_extent() {
|
|
return list_visible_rows() * LIST_ROW_H;
|
|
}
|
|
|
|
static int list_content_extent() {
|
|
return list_count() * LIST_ROW_H;
|
|
}
|
|
|
|
static bool list_scrollbar_visible() {
|
|
return mtk::scrollbar_needed(list_content_extent(), list_view_extent());
|
|
}
|
|
|
|
static Rect list_scrollbar_track() {
|
|
if (!list_scrollbar_visible()) return {0, 0, 0, 0};
|
|
return mtk::scrollbar_track_rect(list_rect());
|
|
}
|
|
|
|
static Rect list_scrollbar_thumb() {
|
|
Rect track = list_scrollbar_track();
|
|
if (track.empty()) return {0, 0, 0, 0};
|
|
return mtk::scrollbar_thumb_rect(track, list_content_extent(),
|
|
list_view_extent(), g_list_scroll * LIST_ROW_H);
|
|
}
|
|
|
|
static Rect list_row_rect(int visibleIndex) {
|
|
Rect list = list_rect();
|
|
int right = list.w - (list_scrollbar_visible() ? mtk::SCROLLBAR_W + 6 : PAD);
|
|
return {PAD, list.y + LIST_TOP_INSET + visibleIndex * LIST_ROW_H,
|
|
right - PAD, LIST_ROW_H};
|
|
}
|
|
|
|
static int pane_x() {
|
|
return sidebar_w() + 1 + PAD;
|
|
}
|
|
|
|
static int pane_w() {
|
|
return g_win.width - pane_x() - PAD;
|
|
}
|
|
|
|
static Rect reload_button() {
|
|
Rect foot = footer_rect();
|
|
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);
|
|
}
|
|
|
|
static void clamp_list_scroll() {
|
|
int max_scroll = list_count() - list_visible_rows();
|
|
if (max_scroll < 0) max_scroll = 0;
|
|
if (g_list_scroll > max_scroll) g_list_scroll = max_scroll;
|
|
if (g_list_scroll < 0) g_list_scroll = 0;
|
|
}
|
|
|
|
static void set_status(const char* msg) {
|
|
snprintf(g_status, sizeof(g_status), "%s", msg ? msg : "");
|
|
g_status_time = montauk::get_milliseconds();
|
|
}
|
|
|
|
static bool status_visible() {
|
|
return g_status[0] && montauk::get_milliseconds() - g_status_time < 5000;
|
|
}
|
|
|
|
// ==== Drawing ====
|
|
|
|
static void fit_text(char* out, int outCap, const char* text, int maxWidth) {
|
|
if (!out || outCap <= 0) return;
|
|
out[0] = '\0';
|
|
if (!text || !text[0] || maxWidth <= 0) return;
|
|
if (text_width(text) <= maxWidth) {
|
|
snprintf(out, outCap, "%s", text);
|
|
return;
|
|
}
|
|
|
|
static const char* ellipsis = "...";
|
|
int len = 0;
|
|
while (text[len] && len < outCap - 4) {
|
|
out[len] = text[len];
|
|
out[len + 1] = '\0';
|
|
if (text_width(out) + text_width(ellipsis) > maxWidth) break;
|
|
len++;
|
|
}
|
|
len = montauk::utf8::floor_boundary(text, montauk::slen(text), len);
|
|
out[len++] = '.';
|
|
out[len++] = '.';
|
|
out[len++] = '.';
|
|
out[len] = '\0';
|
|
}
|
|
|
|
static void draw_text_fit(Canvas& c, int x, int y, const char* text,
|
|
int maxWidth, Color color) {
|
|
char fitted[160];
|
|
fit_text(fitted, sizeof(fitted), text, maxWidth);
|
|
c.text(x, y, fitted, color);
|
|
}
|
|
|
|
static void draw_list(Canvas& c, const mtk::Theme& theme) {
|
|
int fh = system_font_height();
|
|
int count = list_count();
|
|
int visible = list_visible_rows();
|
|
for (int row = 0; row < visible; row++) {
|
|
int index = g_list_scroll + row;
|
|
if (index >= count) break;
|
|
Rect r = list_row_rect(row);
|
|
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
|
|
: 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);
|
|
}
|
|
}
|
|
|
|
if (list_scrollbar_visible()) {
|
|
Rect track = list_scrollbar_track();
|
|
bool hovered = track.contains(g_mouse_x, g_mouse_y) || g_list_sb_dragging;
|
|
mtk::draw_scrollbar(c, track, list_scrollbar_thumb(), hovered,
|
|
g_list_sb_dragging, theme);
|
|
}
|
|
|
|
int sx = sidebar_w();
|
|
c.fill_rect(sx, TAB_H, 1, footer_rect().y - TAB_H, theme.border);
|
|
}
|
|
|
|
static void draw_mark(Canvas& c, int cx, int cy, bool on, const mtk::Theme& theme) {
|
|
Rect cell = {cx - 8, cy - 8, 16, 16};
|
|
mtk::draw_checkbox(c, cell, "", mtk::check_state(on), theme, true, false);
|
|
}
|
|
|
|
static void draw_programs_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_grant_count == 0) {
|
|
c.text(x, y, g_file_present ? "No grants are declared."
|
|
: "0:/config/capabilities.toml was not found.",
|
|
theme.text_subtle);
|
|
c.text(x, y + fh + 8, "Programs without a grant run with no capabilities.",
|
|
theme.text_muted);
|
|
return;
|
|
}
|
|
|
|
const Grant& g = g_grants[g_selected_grant];
|
|
char title[64];
|
|
snprintf(title, sizeof(title), "[grant.%s]", g.id);
|
|
c.text(x, y, title, theme.text);
|
|
|
|
if (g.issue_count > 0) {
|
|
const char* state = "Needs attention";
|
|
c.text(x + w - text_width(state), y, state, theme.danger);
|
|
}
|
|
|
|
y += fh + 6;
|
|
draw_text_fit(c, x, y, g.path[0] ? g.path : "(no path)", w, theme.text_subtle);
|
|
|
|
y += fh + 14;
|
|
mtk::draw_separator(c, x, y, w, theme);
|
|
y += 12;
|
|
|
|
// Grid: one row per capability, one column per mask.
|
|
int colX = x + w - GRID_COL_W * FIELD_COUNT;
|
|
c.text(x, y, "CAPABILITY", theme.text_muted);
|
|
for (int f = 0; f < FIELD_COUNT; f++) {
|
|
const char* label = kFieldLabels[f];
|
|
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
|
|
c.text(cx - text_width(label) / 2, y, label, theme.text_muted);
|
|
}
|
|
y += fh + 4;
|
|
for (int f = 0; f < FIELD_COUNT; f++) {
|
|
const char* note = nullptr;
|
|
if (g.uses_all[f]) note = "(all)";
|
|
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);
|
|
}
|
|
y += fh + 4;
|
|
|
|
for (int i = 0; i < CAP_COUNT; i++) {
|
|
uint64_t bit = montauk::caps::NAMES[i].bit;
|
|
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,
|
|
any ? theme.text : theme.text_muted);
|
|
for (int f = 0; f < FIELD_COUNT; f++) {
|
|
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
|
|
draw_mark(c, cx, y + GRID_ROW_H / 2, (g.mask[f] & bit) != 0, theme);
|
|
}
|
|
y += GRID_ROW_H;
|
|
}
|
|
|
|
y += 10;
|
|
for (int i = 0; i < g.issue_count; i++) {
|
|
draw_text_fit(c, x, y, g.issues[i], w, theme.danger);
|
|
y += fh + 4;
|
|
}
|
|
}
|
|
|
|
static int draw_holder_list(Canvas& c, int x, int y, int w, int bottom,
|
|
const char* heading, int field, uint64_t bit,
|
|
const mtk::Theme& theme) {
|
|
int fh = system_font_height();
|
|
c.text(x, y, heading, theme.text_muted);
|
|
y += fh + 8;
|
|
|
|
int total = 0;
|
|
for (int i = 0; i < g_grant_count; i++)
|
|
if (g_grants[i].mask[field] & bit) total++;
|
|
if (total == 0) {
|
|
c.text(x + 10, y, "No program", theme.text_subtle);
|
|
return y + fh + 16;
|
|
}
|
|
|
|
int shown = 0;
|
|
for (int i = 0; i < g_grant_count; i++) {
|
|
const Grant& g = g_grants[i];
|
|
if (!(g.mask[field] & bit)) continue;
|
|
if (y + (fh + 6) * 2 > bottom && shown < total - 1) {
|
|
char more[48];
|
|
snprintf(more, sizeof(more), "+%d more", total - shown);
|
|
c.text(x + 10, y, more, theme.text_muted);
|
|
return y + fh + 16;
|
|
}
|
|
c.text(x + 10, y, g.id, theme.text);
|
|
draw_text_fit(c, x + 130, y, g.path, w - 130, theme.text_subtle);
|
|
y += fh + 6;
|
|
shown++;
|
|
}
|
|
return y + 12;
|
|
}
|
|
|
|
static void draw_caps_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;
|
|
int bottom = footer_rect().y - PAD;
|
|
|
|
const auto& cap = montauk::caps::NAMES[g_selected_cap];
|
|
c.text(x, y, cap.name, theme.text);
|
|
y += fh + 6;
|
|
draw_text_fit(c, x, y, kCapDescriptions[g_selected_cap], w, theme.text_subtle);
|
|
y += fh + 14;
|
|
mtk::draw_separator(c, x, y, w, theme);
|
|
y += 14;
|
|
|
|
y = draw_holder_list(c, x, y, w, bottom - (fh + 6) * 4,
|
|
"USES IT (EFFECTIVE)", FIELD_EFFECTIVE, cap.bit, theme);
|
|
draw_holder_list(c, x, y, w, bottom,
|
|
"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;
|
|
const char* title = "Administrator access required";
|
|
c.text((g_win.width - text_width(title)) / 2, y, title, theme.text);
|
|
const char* body = "Only administrators can review the capability grant table.";
|
|
c.text((g_win.width - text_width(body)) / 2, y + fh + 10, body, theme.text_subtle);
|
|
}
|
|
|
|
static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
|
Rect foot = footer_rect();
|
|
c.fill_rect(foot.x, foot.y, foot.w, foot.h, theme.surface);
|
|
mtk::draw_separator(c, 0, foot.y, g_win.width, theme);
|
|
|
|
char summary[128];
|
|
if (status_visible()) {
|
|
snprintf(summary, sizeof(summary), "%s", g_status);
|
|
} else if (!g_is_admin) {
|
|
snprintf(summary, sizeof(summary), "Read-only");
|
|
} else {
|
|
int flagged = 0;
|
|
for (int i = 0; i < g_grant_count; i++)
|
|
if (g_grants[i].issue_count > 0) 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);
|
|
|
|
if (g_is_admin) {
|
|
Rect reload = reload_button();
|
|
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() {
|
|
mtk::StandaloneHost host(&g_win);
|
|
Canvas c = host.canvas();
|
|
mtk::Theme theme = app_theme();
|
|
c.fill(theme.window_bg);
|
|
if (g_is_admin) {
|
|
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);
|
|
}
|
|
draw_footer(c, theme);
|
|
host.present();
|
|
}
|
|
|
|
// ==== Input ====
|
|
|
|
static void reload() {
|
|
load_grants();
|
|
load_decisions();
|
|
clamp_list_scroll();
|
|
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) {
|
|
int count = list_count();
|
|
if (count <= 0) return;
|
|
if (index < 0) index = 0;
|
|
if (index >= count) index = count - 1;
|
|
*selected_for_tab() = index;
|
|
|
|
int visible = list_visible_rows();
|
|
if (index < g_list_scroll) g_list_scroll = index;
|
|
else if (index >= g_list_scroll + visible) g_list_scroll = index - visible + 1;
|
|
clamp_list_scroll();
|
|
}
|
|
|
|
static void switch_tab(Tab tab) {
|
|
g_tab = tab;
|
|
g_list_scroll = 0;
|
|
select_index(*selected_for_tab());
|
|
}
|
|
|
|
static void handle_mouse(const montauk::abi::WinEvent& ev) {
|
|
if (!g_is_admin) return;
|
|
int mx = ev.mouse.x;
|
|
int my = ev.mouse.y;
|
|
bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
|
|
|
Rect track = list_scrollbar_track();
|
|
Rect thumb = list_scrollbar_thumb();
|
|
if (g_list_sb_dragging) {
|
|
if ((ev.mouse.buttons & 1) && !track.empty()) {
|
|
int offset = mtk::scrollbar_offset_from_thumb_top(
|
|
track, list_content_extent(), list_view_extent(),
|
|
thumb.h, my - g_list_sb_drag_offset);
|
|
g_list_scroll = offset / LIST_ROW_H;
|
|
clamp_list_scroll();
|
|
return;
|
|
}
|
|
g_list_sb_dragging = false;
|
|
}
|
|
|
|
if (ev.mouse.scroll != 0 && list_rect().contains(mx, my)) {
|
|
g_list_scroll += ev.mouse.scroll > 0 ? -1 : 1;
|
|
clamp_list_scroll();
|
|
return;
|
|
}
|
|
if (!pressed) return;
|
|
|
|
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
|
|
if (tab >= 0) {
|
|
switch_tab((Tab)tab);
|
|
return;
|
|
}
|
|
if (!track.empty() && track.contains(mx, my)) {
|
|
if (thumb.contains(mx, my)) {
|
|
g_list_sb_dragging = true;
|
|
g_list_sb_drag_offset = my - thumb.y;
|
|
} else {
|
|
int visible = list_visible_rows();
|
|
int page = visible > 1 ? visible - 1 : 1;
|
|
g_list_scroll += my < thumb.y ? -page : page;
|
|
clamp_list_scroll();
|
|
}
|
|
return;
|
|
}
|
|
for (int row = 0; row < list_visible_rows(); row++) {
|
|
int index = g_list_scroll + row;
|
|
if (index >= list_count()) break;
|
|
if (list_row_rect(row).contains(mx, my)) {
|
|
select_index(index);
|
|
return;
|
|
}
|
|
}
|
|
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) {
|
|
if (!key.pressed) return false;
|
|
if (key.scancode == 0x01) {
|
|
g_win.closed = true;
|
|
return true;
|
|
}
|
|
if (!g_is_admin) return false;
|
|
if (key.ascii == '\t') {
|
|
switch_tab((Tab)((g_tab + 1) % TAB_COUNT));
|
|
return true;
|
|
}
|
|
int current = *selected_for_tab();
|
|
if (montauk::nav_scancode(key) == 0x48) { select_index(current - 1); return true; } // Up
|
|
if (montauk::nav_scancode(key) == 0x50) { select_index(current + 1); return true; } // Down
|
|
if (key.ascii == 'r' || key.ascii == 'R') {
|
|
reload();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
extern "C" void _start() {
|
|
if (!fonts::init()) montauk::exit(1);
|
|
g_accent = mtk::load_system_accent();
|
|
|
|
g_is_admin = session_is_admin();
|
|
if (g_is_admin) {
|
|
load_grants();
|
|
load_decisions();
|
|
}
|
|
|
|
if (!g_win.create("Capabilities", WIN_W, WIN_H)) montauk::exit(1);
|
|
render();
|
|
|
|
while (g_win.id >= 0 && !g_win.closed) {
|
|
montauk::abi::WinEvent ev;
|
|
int result = g_win.poll(&ev);
|
|
if (result < 0) break;
|
|
if (result == 0) {
|
|
// Keep drawing while a status message is up, plus once more to
|
|
// clear it when it expires.
|
|
static bool status_drawn = false;
|
|
if (status_visible() || status_drawn) {
|
|
status_drawn = status_visible();
|
|
render();
|
|
}
|
|
montauk::sleep_ms(16);
|
|
continue;
|
|
}
|
|
|
|
bool redraw = false;
|
|
if (ev.type == 3) break;
|
|
if (ev.type == 2 || ev.type == 4) {
|
|
clamp_list_scroll();
|
|
redraw = true;
|
|
} else if (ev.type == 1) {
|
|
g_mouse_x = ev.mouse.x;
|
|
g_mouse_y = ev.mouse.y;
|
|
handle_mouse(ev);
|
|
redraw = true;
|
|
} else if (ev.type == 0) {
|
|
redraw = handle_key(ev.key);
|
|
}
|
|
if (redraw) render();
|
|
}
|
|
|
|
g_win.destroy();
|
|
montauk::exit(0);
|
|
}
|