feat: use polkit auth for admin actions (#1109)

This commit is contained in:
Ilya Zlobintsev
2026-07-10 23:00:34 +03:00
committed by GitHub
parent 2f6bbfa3bc
commit 4bf5bc6166
8 changed files with 167 additions and 25 deletions
Generated
+26 -12
View File
@@ -75,7 +75,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -86,7 +86,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -672,7 +672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1092,7 +1092,7 @@ dependencies = [
"gobject-sys",
"libc",
"system-deps",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1601,7 +1601,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1734,6 +1734,7 @@ dependencies = [
"tracing-subscriber",
"ureq",
"zbus",
"zbus_polkit",
]
[[package]]
@@ -2098,7 +2099,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2602,7 +2603,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2853,7 +2854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2960,7 +2961,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2970,7 +2971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -3281,7 +3282,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -3584,7 +3585,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4091,6 +4092,19 @@ dependencies = [
"zvariant",
]
[[package]]
name = "zbus_polkit"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52"
dependencies = [
"enumflags2",
"serde",
"serde_repr",
"static_assertions",
"zbus",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
+2
View File
@@ -22,6 +22,7 @@ install-resources:
install -Dm644 res/io.github.ilya_zlobintsev.LACT.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/512x512/apps/io.github.ilya_zlobintsev.LACT.png
install -Dm644 res/io.github.ilya_zlobintsev.LACT.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/io.github.ilya_zlobintsev.LACT.svg
install -Dm644 res/io.github.ilya_zlobintsev.LACT.metainfo.xml $(DESTDIR)$(PREFIX)/share/metainfo/io.github.ilya_zlobintsev.LACT.metainfo.xml
install -Dm644 res/io.github.ilya_zlobintsev.LACT.policy $(DESTDIR)$(PREFIX)/share/polkit-1/actions/io.github.ilya_zlobintsev.LACT.policy
.PHONY: install
install: install-resources
@@ -40,6 +41,7 @@ uninstall:
rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/512x512/apps/io.github.ilya_zlobintsev.LACT.png
rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/io.github.ilya_zlobintsev.LACT.svg
rm $(DESTDIR)$(PREFIX)/share/metainfo/io.github.ilya_zlobintsev.LACT.metainfo.xml
rm $(DESTDIR)$(PREFIX)/share/polkit-1/actions/io.github.ilya_zlobintsev.LACT.policy
.PHONY: update-vulkan-schema
update-vulkan-schema:
+13
View File
@@ -5238,6 +5238,19 @@
"dest": "cargo/vendor/zbus_names-4.3.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/zbus_polkit/zbus_polkit-5.0.0.crate",
"sha256": "ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52",
"dest": "cargo/vendor/zbus_polkit-5.0.0"
},
{
"type": "inline",
"contents": "{\"package\": \"ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52\", \"files\": {}}",
"dest": "cargo/vendor/zbus_polkit-5.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
+1
View File
@@ -38,6 +38,7 @@ nvml-wrapper = "0.12.1"
bitflags = "2.11.1"
pciid-parser = { version = "0.8", features = ["serde"] }
zbus = { version = "5.14.0", default-features = false, features = ["tokio"] }
zbus_polkit = { version = "5.0.0", default-features = false, features = ["tokio"] }
libdrm_amdgpu_sys = { version = "0.8.16", default-features = false, features = [
"dynamic_loading",
] }
+3 -1
View File
@@ -28,6 +28,7 @@ use tracing::level_filters::LevelFilter;
use tracing::{Instrument, debug_span, error, info, warn};
use tracing_subscriber::EnvFilter;
use crate::server::ClientContext;
pub use system::BASE_MODULE_CONF_PATH;
const DRM_EVENT_TIMEOUT_PERIOD_MS: u64 = 100;
@@ -83,8 +84,9 @@ pub fn run_embedded(stream: StdUnixStream) -> anyhow::Result<()> {
let config = Config::default();
let handler = Handler::new(config).await?;
let stream = UnixStream::try_from(stream)?;
let ctx = ClientContext::default();
handle_stream(stream, handler).await
handle_stream(stream, handler, ctx).await
})
}
+22 -6
View File
@@ -12,6 +12,7 @@ use crate::{config::Config, socket, system};
use anyhow::Context;
use futures::future::join_all;
use lact_schema::{Pong, Request, Response};
use nix::sys::socket::{getsockopt, sockopt::PeerCredentials};
use serde::Serialize;
use std::fmt::Debug;
use tokio::{
@@ -19,7 +20,7 @@ use tokio::{
net::{TcpListener, UnixListener},
sync::Notify,
};
use tracing::{debug, error, info, instrument, trace};
use tracing::{debug, error, info, instrument, trace, warn};
pub struct Server {
pub handler: Handler,
@@ -74,9 +75,15 @@ impl Server {
loop {
match self.unix_listener.accept().await {
Ok((stream, _)) => {
let client_credentials = getsockopt(&stream, PeerCredentials)
.inspect_err(|err| warn!("could not get client credentials: {err:#}"))
.ok();
let ctx = ClientContext {
client_pid: client_credentials.map(|creds| creds.pid().cast_unsigned()),
};
let handler = unix_handler.clone();
tokio::task::spawn_local(async move {
if let Err(error) = handle_stream(stream, handler).await {
if let Err(error) = handle_stream(stream, handler, ctx).await {
error!("{error}");
}
});
@@ -94,9 +101,11 @@ impl Server {
loop {
match tcp_listener.accept().await {
Ok((stream, _)) => {
let ctx = ClientContext::default();
let handler = self.handler.clone();
tokio::task::spawn_local(async move {
if let Err(error) = handle_stream(stream, handler).await {
if let Err(error) = handle_stream(stream, handler, ctx).await {
error!("{error}");
}
});
@@ -114,10 +123,16 @@ impl Server {
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct ClientContext {
pub client_pid: Option<u32>,
}
#[instrument(level = "debug", skip(stream, handler))]
pub async fn handle_stream<T: AsyncRead + AsyncWrite + Unpin>(
stream: T,
handler: Handler,
ctx: ClientContext,
) -> anyhow::Result<()> {
let disconnect_notify = std::sync::Arc::new(Notify::new());
@@ -129,7 +144,7 @@ pub async fn handle_stream<T: AsyncRead + AsyncWrite + Unpin>(
let maybe_request = serde_json::from_str(&buf);
let response = match maybe_request {
Ok(request) => match handle_request(request, &handler, &disconnect_notify).await {
Ok(request) => match handle_request(request, &handler, &disconnect_notify, ctx).await {
Ok(response) => response,
Err(error) => serde_json::to_vec(&Response::<()>::from(error))?,
},
@@ -154,6 +169,7 @@ async fn handle_request<'a>(
request: Request<'a>,
handler: &'a Handler,
disconnect_notify: &std::sync::Arc<Notify>,
ctx: ClientContext,
) -> anyhow::Result<Vec<u8>> {
match request {
Request::Ping => ok_response(ping()),
@@ -209,7 +225,7 @@ async fn handle_request<'a>(
.await?,
),
Request::CreateProfile { name, base } => {
ok_response(handler.create_profile(name, base).await?)
ok_response(handler.create_profile(name, base, ctx).await?)
}
Request::DeleteProfile { name } => ok_response(handler.delete_profile(name).await?),
Request::MoveProfile { name, new_position } => {
@@ -223,7 +239,7 @@ async fn handle_request<'a>(
Request::ReleaseProfile { cookie } => ok_response(handler.release_profile(cookie).await?),
Request::EvaluateProfileRule { rule } => ok_response(handler.evaluate_profile_rule(&rule)?),
Request::SetProfileRule { name, rule, hooks } => {
ok_response(handler.set_profile_rule(&name, rule, hooks).await?)
ok_response(handler.set_profile_rule(&name, rule, hooks, ctx).await?)
}
Request::GetGpuConfig { id } => ok_response(handler.get_gpu_config(id).await?),
Request::SetGpuConfig { id, config } => {
+87 -6
View File
@@ -8,7 +8,7 @@ use crate::server::display;
use crate::{
bindings::intel::IntelDrm,
config::Config,
server::{gpu_controller::init_controller, profiles, system::DAEMON_VERSION},
server::{ClientContext, gpu_controller::init_controller, profiles, system::DAEMON_VERSION},
};
use crate::{server::gpu_controller::NvidiaLibs, system::run_command};
use amdgpu_sysfs::gpu_handle::{
@@ -32,13 +32,11 @@ use nix::libc;
use nvml_wrapper::Nvml;
use pciid_parser::Database;
use serde_json::json;
#[cfg(not(test))]
use std::collections::HashMap;
#[cfg(all(not(test), feature = "nvidia"))]
use std::sync::Arc;
use std::{
cell::{Cell, RefCell},
collections::BTreeMap,
collections::{BTreeMap, HashMap},
env,
fs::{self, File, Permissions},
io::{BufWriter, Cursor, Write},
@@ -55,6 +53,7 @@ use tokio::{
time::sleep,
};
use tracing::{debug, error, info, trace, warn};
use zbus_polkit::policykit1::{self, AuthorityProxy};
const CONTROLLERS_LOAD_RETRY_ATTEMPTS: u8 = 5;
const CONTROLLERS_LOAD_RETRY_INTERVAL: u64 = 3;
@@ -86,6 +85,10 @@ const SNAPSHOT_EXCLUDED_FILENAME_PREFIXES: &[&str] = &[
];
const CONFIG_RESET_CMDLINE_ARG: &str = "lact-reset";
mod polkit_actions {
pub const PROFILE_HOOK: &str = "io.github.ilya_zlobintsev.LACT.profile-hook";
}
type ProfileHolds = Rc<RefCell<Vec<(u64, Rc<str>, mpsc::Sender<()>)>>>;
type ProfileHoldSnapshot = Rc<RefCell<Option<(Option<Rc<str>>, bool)>>>;
@@ -101,6 +104,7 @@ pub struct Handler {
profile_holds: ProfileHolds,
profile_hold_snapshot: ProfileHoldSnapshot,
next_hold_cookie: Rc<Cell<u64>>,
polkit_proxy: Option<AuthorityProxy<'static>>,
}
impl<'a> Handler {
@@ -178,6 +182,13 @@ impl<'a> Handler {
config.save(&Cell::new(Instant::now()))?;
}
let polkit_proxy = connect_polkit_proxy()
.await
.inspect_err(|err| {
warn!("could not connect to polkit, admin checks will not be available: {err:#}");
})
.ok();
let handler = Self {
gpu_controllers: Rc::new(RwLock::new(controllers)),
config: Rc::new(RwLock::new(config)),
@@ -189,6 +200,7 @@ impl<'a> Handler {
profile_holds: Rc::new(RefCell::new(Vec::new())),
profile_hold_snapshot: Rc::new(RefCell::new(None)),
next_hold_cookie: Rc::new(Cell::new(1)),
polkit_proxy,
};
if let Err(err) = handler.apply_current_config().await {
error!("could not apply config: {err:#}");
@@ -913,7 +925,12 @@ impl<'a> Handler {
Ok(())
}
pub async fn create_profile(&self, name: String, base: ProfileBase) -> anyhow::Result<()> {
pub async fn create_profile(
&self,
name: String,
base: ProfileBase,
ctx: ClientContext,
) -> anyhow::Result<()> {
{
let mut config = self.config.write().await;
if config.profiles.contains_key(name.as_str()) {
@@ -924,7 +941,18 @@ impl<'a> Handler {
ProfileBase::Empty => Profile::default(),
ProfileBase::Default => config.default_profile(),
ProfileBase::Profile(name) => config.profile(&name)?.clone(),
ProfileBase::Provided(profile) => profile,
ProfileBase::Provided(profile) => {
if !profile.hooks.is_empty() {
self.check_auth(
polkit_actions::PROFILE_HOOK,
"User was not authorized to set profile rule hooks",
ctx,
)
.await?;
}
profile
}
};
config.profiles.insert(name.into(), profile);
config.save(&self.config_last_saved)?;
@@ -996,7 +1024,17 @@ impl<'a> Handler {
name: &str,
rule: Option<ProfileRule>,
hooks: ProfileHooks,
ctx: ClientContext,
) -> anyhow::Result<()> {
if !hooks.is_empty() {
self.check_auth(
polkit_actions::PROFILE_HOOK,
"User was not authorized to set profile rule hooks",
ctx,
)
.await?;
}
{
let mut config = self.config.write().await;
let profile = config
@@ -1195,6 +1233,37 @@ impl<'a> Handler {
controller.cleanup().await;
}
}
async fn check_auth(
&self,
action: &str,
error_msg: &str,
ctx: ClientContext,
) -> anyhow::Result<()> {
let polkit_proxy = self
.polkit_proxy
.as_ref()
.context("Polkit not available, cannot ask for authorization")?;
let pid = ctx.client_pid.context("No client PID available")?;
let subject = policykit1::Subject::new_for_owner(pid, None, None)?;
let result = polkit_proxy
.check_authorization(
&subject,
action,
&HashMap::new(),
policykit1::CheckAuthorizationFlags::AllowUserInteraction.into(),
"",
)
.await
.context("Authorization could not be granted")?;
if result.is_authorized {
Ok(())
} else {
bail!("{error_msg}");
}
}
}
async fn apply_config_to_controllers(
@@ -1495,3 +1564,15 @@ fn controller_vendor_is_unique(
== *vendor_id
})
}
async fn connect_polkit_proxy() -> anyhow::Result<AuthorityProxy<'static>> {
let conn = zbus::Connection::system()
.await
.context("Could not establish dbus connection")?;
let proxy = AuthorityProxy::new(&conn)
.await
.context("Could not connect to polkit")?;
Ok(proxy)
}
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<action id="io.github.ilya_zlobintsev.LACT.profile-hook">
<description>Set LACT profile hooks</description>
<message>Setting root profile hooks requires authorization</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>