Implement password handling for unattended access
build / build-linux-amd64 (push) Successful in 2m0s

This commit is contained in:
2026-05-08 09:32:13 +02:00
parent c1eaac1cb3
commit 0af4f5edce
7 changed files with 168 additions and 14 deletions
+47 -2
View File
@@ -205,6 +205,13 @@ pub struct DashboardDeviceRow {
pub last_heartbeat_at: String,
pub sysinfo_payload: String,
pub conns_json: String,
/// Plaintext per-boot password reported by the agent for unattended
/// access. Empty when the agent hasn't reported one (vanilla rustdesk
/// or hello-agent that hasn't called the endpoint yet). The admin UI
/// only surfaces this when the row is online AND no interactive user
/// is logged in.
pub unattended_password: String,
pub unattended_password_set_at: String,
}
#[derive(Debug, Clone, Default)]
@@ -368,7 +375,9 @@ impl Database {
}
// Soft-ALTERs run after schema creation. SQLite < 3.35 lacks
// `ADD COLUMN IF NOT EXISTS`; swallow the duplicate-column error
// so re-runs are idempotent.
// so re-runs are idempotent. Newly-added soft alters get appended
// to the same list — order doesn't matter beyond "after the table
// they touch exists in M*_SCHEMA".
for stmt in M2_SOFT_ALTERS {
self.try_alter(stmt).await;
}
@@ -604,7 +613,9 @@ impl Database {
COALESCE(u.username, '') AS owner_username, \
ds.last_heartbeat_at AS last_hb, \
ds.payload AS payload, \
ds.conns AS conns \
ds.conns AS conns, \
COALESCE(ds.unattended_password, '') AS u_pw, \
COALESCE(ds.unattended_password_set_at, '') AS u_pw_at \
FROM device_sysinfo ds \
LEFT JOIN users u ON u.id = ds.user_id \
ORDER BY ds.last_heartbeat_at DESC LIMIT ? OFFSET ?",
@@ -622,6 +633,8 @@ impl Database {
last_heartbeat_at: r.try_get("last_hb").unwrap_or_default(),
sysinfo_payload: r.try_get("payload").unwrap_or_default(),
conns_json: r.try_get("conns").unwrap_or_default(),
unattended_password: r.try_get("u_pw").unwrap_or_default(),
unattended_password_set_at: r.try_get("u_pw_at").unwrap_or_default(),
})
.collect();
Ok((total, data))
@@ -1116,6 +1129,31 @@ impl Database {
Ok(())
}
/// Store the agent's per-boot unattended-access password. Upserts so a
/// device that's never sysinfo'd yet still gets a `device_sysinfo` row
/// to hang the password on. Caller is expected to have validated the
/// (id, uuid) pair against `peer` first — same gate as sysinfo_upsert.
pub async fn set_unattended_password(
&self,
id: &str,
uuid: &str,
password: &str,
) -> ResultType<()> {
sqlx::query(
"INSERT INTO device_sysinfo(id, uuid, unattended_password, unattended_password_set_at) \
VALUES(?, ?, ?, current_timestamp) \
ON CONFLICT(id, uuid) DO UPDATE SET \
unattended_password = excluded.unattended_password, \
unattended_password_set_at = current_timestamp",
)
.bind(id)
.bind(uuid)
.bind(password)
.execute(self.pool.get().await?.deref_mut())
.await?;
Ok(())
}
// ===================================================================
// M2: address book / tags / device groups / accessibility
// ===================================================================
@@ -2971,6 +3009,13 @@ const M2_SOFT_ALTERS: &[&str] = &[
// login — promotion AND demotion at the IdP propagate.
"ALTER TABLE oidc_providers ADD COLUMN admin_role TEXT",
"ALTER TABLE oidc_providers ADD COLUMN roles_claim TEXT",
// Unattended-access password. Some agents (hello-agent) generate a
// random "permanent password" on every boot and report it back here
// so a supporter can reach the box when no user is logged in to
// approve a connection. Stored as plaintext on purpose: the admin UI
// displays it for the operator to read, and it rotates each boot.
"ALTER TABLE device_sysinfo ADD COLUMN unattended_password TEXT",
"ALTER TABLE device_sysinfo ADD COLUMN unattended_password_set_at DATETIME",
];
const M3_SCHEMA: &[&str] = &[