Turn the rows that only reported things into controls

Autostart entries showed "Enabled" or "Disabled" as plain text. The row did
toggle on click the whole time, so this is an affordance rather than a missing
capability -- but a control that reads as static text is one nobody knows they
have. It is a switch now, with removal alongside it behind a confirmation:
disabling writes Hidden=true and can be undone, deleting the file cannot.

remove-autostart is confined to files the autostart directory owns. It resolves
the path and compares the parent, so a name like "../../.bashrc" cannot escape,
and it refuses symlinks rather than following them -- deleting through one would
remove whatever it points at, which is somewhere else and not ours. Each refusal
was tested against a fixture directory, including a symlink aimed at
/etc/hostname, which survived.

Sharing says who is signed in from another machine: user, origin and since when.
An empty list on this machine proves nothing, so the parser was checked against
sample `who` output -- it picks out remote sessions and leaves out local seats
and the :0 display, which would otherwise report the person at the keyboard as a
remote login.

Media sharing was "Available" and nothing else: rygel installed, rygel.service
disabled, no way to change that from here. It is a switch now, and it says what
it does before you touch it rather than afterwards -- turning it on publishes
media folders to every device on the network with no password in front of them.

Per-application camera and microphone permissions come from the portal's
permission store, which is where an application that asked through the portal
has its answer recorded. The page states the limit plainly instead of implying a
protection that does not exist: a program installed outside the portal opens the
device directly and nothing here stands in its way. Anything that is not an
explicit "yes" is treated as withheld, because guessing generously about a
camera is the wrong way to be wrong.

The first version of the write silently did nothing -- SetPermission takes an
array of strings and was being handed one string -- and the test did not notice,
because it discarded the helper's output and only checked that state was
unchanged afterwards, which was trivially true. The contract now requires the
value to move, and was proven to fail by putting that exact bug back.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-19 23:10:48 -04:00
parent 7e1c85b094
commit f6b970da21
10 changed files with 664 additions and 11 deletions
@@ -13,6 +13,10 @@ SettingsPage {
property string expandedRole: ""
property bool addingAutostart: false
// Which entry has been asked to be removed. Removal deletes a file, so
// it never happens on a first press.
property string confirmingAutostartRemoval: ""
readonly property var applications: DesktopEntries.applications.values
// Each role governs a whole family of types, not one representative: setting
// "Images" writes PNG, JPEG, WebP and the rest together, so a file manager
@@ -268,12 +272,50 @@ SettingsPage {
required property var modelData
required property int index
readonly property bool confirming:
root.confirmingAutostartRemoval === String(autostartRow.modelData.id)
label: autostartRow.modelData.name
detail: autostartRow.modelData.id
value: autostartRow.modelData.enabled ? "Enabled" : "Disabled"
activatable: !DefaultApps.busy
detail: autostartRow.confirming
? "Removing deletes this entry. Turning it off instead is reversible."
: autostartRow.modelData.id
divider: autostartRow.index < DefaultApps.autostartEntries.length - 1
onActivated: DefaultApps.setAutostart(autostartRow.modelData.id, !autostartRow.modelData.enabled)
controlWidth: 210
// A switch, not the words "Enabled"/"Disabled". The row always
// toggled on click, but read as static text, so a control that
// worked looked like a status nobody could change.
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 9
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
visible: autostartRow.confirming
text: "Remove it"
tone: "danger"
enabled: !DefaultApps.busy
onClicked: {
root.confirmingAutostartRemoval = "";
DefaultApps.removeAutostart(String(autostartRow.modelData.id));
}
}
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: autostartRow.confirming ? "Keep" : "Remove…"
enabled: !DefaultApps.busy
onClicked: root.confirmingAutostartRemoval =
autostartRow.confirming ? "" : String(autostartRow.modelData.id)
}
SettingsToggle {
anchors.verticalCenter: parent.verticalCenter
checked: autostartRow.modelData.enabled
onToggled: value => DefaultApps.setAutostart(autostartRow.modelData.id, value)
}
}
}
}
}
@@ -47,6 +47,8 @@ SettingsPage {
DeviceSecurity.refresh();
if (!Keyring.scanned)
Keyring.refresh();
if (!Permissions.scanned)
Permissions.refresh();
}
SettingsCard {
@@ -281,6 +283,70 @@ SettingsPage {
}
}
SettingsCard {
title: "Application permissions"
// The limit is stated here rather than left to be discovered. Saying
// "your camera is protected" when a native binary can open it
// directly would be a claim this page cannot back up.
subtitle: Permissions.available
? "Applications that asked through the desktop portal. Programs installed outside it can still reach these devices directly."
: (Permissions.lastError || "The desktop portal's permission store is not running.")
Repeater {
model: Permissions.devices
delegate: Column {
id: deviceBlock
required property var modelData
readonly property var applications: deviceBlock.modelData.applications ?? []
width: parent.width
TextRow {
width: parent.width
visible: deviceBlock.applications.length === 0
label: String(deviceBlock.modelData.label ?? "")
detail: "No application has asked for this."
value: ""
}
Repeater {
model: deviceBlock.applications
delegate: SettingRow {
required property var modelData
width: parent.width
label: String(modelData.app ?? "")
detail: String(deviceBlock.modelData.label ?? "")
controlWidth: 150
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 9
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: "Ask again"
enabled: !Permissions.busy
onClicked: Permissions.forget(
String(deviceBlock.modelData.id), String(modelData.app))
}
SettingsToggle {
anchors.verticalCenter: parent.verticalCenter
checked: modelData.allowed === true
onToggled: value => Permissions.setAllowed(
String(deviceBlock.modelData.id), String(modelData.app), value)
}
}
}
}
}
}
}
SettingsCard {
title: "Device security"
subtitle: DeviceSecurity.attentionCount === 0
@@ -68,6 +68,25 @@ SettingsPage {
value: "ssh " + Sharing.networkName
}
TextRow {
visible: Sharing.remoteLoginOn && Sharing.remoteSessions.length === 0
label: "Nobody is signed in"
detail: "Remote login is on, and no one is connected from another machine."
value: ""
}
Repeater {
model: Sharing.remoteSessions
delegate: TextRow {
required property var modelData
width: parent.width
label: String(modelData.user ?? "") + " is signed in from " + String(modelData.from ?? "")
detail: "Since " + String(modelData.since ?? "") + " · " + String(modelData.line ?? "")
value: ""
}
}
TextRow {
visible: Sharing.remoteLoginOn
label: "Port"
@@ -165,12 +184,25 @@ SettingsPage {
value: Sharing.fileSharing?.installed === true ? "Available" : "Not installed"
}
TextRow {
SwitchRow {
visible: Sharing.mediaSharing?.installed === true
label: "Share music and video to devices"
detail: Sharing.mediaSharing?.installed === true
? "Rygel is installed"
: "Needs Rygel, which is not installed."
value: Sharing.mediaSharing?.installed === true ? "Available" : "Not installed"
// Said before it happens, not after: this advertises on the network
// to anything that speaks DLNA, with no password in front of it.
detail: Sharing.mediaSharing?.active === true
? "Rygel is serving your media to devices on the network"
: "Publishes your media folders to every device on the network. No password is asked for."
checked: Sharing.mediaSharing?.active === true
enabled: !Sharing.busy
divider: false
onToggled: value => Sharing.setMediaSharing(value)
}
TextRow {
visible: Sharing.mediaSharing?.installed !== true
label: "Share music and video to devices"
detail: "Needs Rygel, which is not installed."
value: "Not installed"
divider: false
}
}