From 33383b21d7eec8f525b7110f4ce7bc5493d99001 Mon Sep 17 00:00:00 2001 From: Emil Burzo Date: Thu, 22 Oct 2026 19:43:30 +0300 Subject: [PATCH 1/3] ak-platform-authz: use a separate polkit action for each operation --- ak-agent/src/grpc/agent_auth.rs | 10 +-- ak-agent/src/ssh/txn.rs | 4 +- ak-platform-authz/examples/authz_prompt.rs | 15 ++++- ak-platform-authz/src/grpc.rs | 9 +-- ak-platform-authz/src/lib.rs | 35 +++++++++-- ak-platform-authz/src/sys/linux.rs | 54 +++++++++++++++- ak-platform-authz/src/sys/macos.rs | 4 +- ak-platform-authz/src/sys/mod.rs | 10 +-- ak-platform-authz/src/sys/windows.rs | 4 +- .../actions/io.goauthentik.platform.policy | 61 +++++++++++++++++++ 10 files changed, 180 insertions(+), 26 deletions(-) diff --git a/ak-agent/src/grpc/agent_auth.rs b/ak-agent/src/grpc/agent_auth.rs index 769c27c6b..1bf5781af 100644 --- a/ak-agent/src/grpc/agent_auth.rs +++ b/ak-agent/src/grpc/agent_auth.rs @@ -13,7 +13,7 @@ use ak_platform::{ oauth2_http, string::PlatformString, }; -use ak_platform_authz::grpc::AuthPeer; +use ak_platform_authz::{Operation, grpc::AuthPeer}; use ak_platform_keyring::cache::Cache; use ak_platform_keyring::cache::CacheData; use chrono::{DateTime, Utc}; @@ -114,7 +114,7 @@ impl AgentAuth for AgentGRPCServer { .await?; request - .auth_peer() + .auth_peer(Operation::WhoAmI) .with_message(|c| { let cmd = c.clone().proc_info()?.parent_cmdline()?; Ok(PlatformString::new() @@ -176,7 +176,7 @@ impl AgentAuth for AgentGRPCServer { .ok_or(Status::invalid_argument("profile not found"))?; request - .auth_peer() + .auth_peer(Operation::CurrentToken) .with_creds(on_behalf_of(&request, inner_req.caller_pid)?) .with_message(|c| { let cmd = c.clone().proc_info()?.parent_cmdline()?; @@ -238,7 +238,7 @@ impl AgentAuth for AgentGRPCServer { let cid1 = audience.clone(); let cid2 = audience.clone(); request - .auth_peer() + .auth_peer(Operation::TokenExchange) .with_message(move |c| { let cmd = c.clone().proc_info()?.parent_cmdline()?; Ok(PlatformString::new() @@ -309,7 +309,7 @@ impl AgentAuth for AgentGRPCServer { let uid = inner.uid.clone(); request - .auth_peer() + .auth_peer(Operation::Authorize) .with_message(move |_c| { Ok(PlatformString::new().with_darwin(format!("authorize access to '{}'", service))) }) diff --git a/ak-agent/src/ssh/txn.rs b/ak-agent/src/ssh/txn.rs index f1b23bb7a..74ea228c3 100644 --- a/ak-agent/src/ssh/txn.rs +++ b/ak-agent/src/ssh/txn.rs @@ -4,7 +4,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use ak_platform::{ http::ReqwestResponseError, net::server::creds::ProcCredentials, string::PlatformString, }; -use ak_platform_authz::AuthorizeAction; +use ak_platform_authz::{AuthorizeAction, Operation}; use authentik_client::apis::endpoints_api::endpoints_agents_connectors_auth_fed_create; use eyre::{Result, WrapErr, bail}; use serde::Deserialize; @@ -209,7 +209,7 @@ impl SSHAgentTransaction { let hk1 = host_key_str.to_string(); let hk2 = host_key_str.to_string(); - let result = AuthorizeAction::build() + let result = AuthorizeAction::build(Operation::SshLogin) .with_message(move |c| { let cmd = c.clone().proc_info()?.parent_cmdline()?; Ok(PlatformString::new() diff --git a/ak-platform-authz/examples/authz_prompt.rs b/ak-platform-authz/examples/authz_prompt.rs index 4d24ae6c6..f7e6b2853 100644 --- a/ak-platform-authz/examples/authz_prompt.rs +++ b/ak-platform-authz/examples/authz_prompt.rs @@ -1,15 +1,26 @@ use std::time::Duration; use ak_platform::{log::LogBuilder, net::server::creds::ProcCredentials, string::PlatformString}; -use ak_platform_authz::AuthorizeAction; +use ak_platform_authz::{AuthorizeAction, Operation}; #[tokio::main] async fn main() { LogBuilder::new(PlatformString::new()) .force_stdout(true) .enable(); + let operation = match std::env::args().nth(1).as_deref() { + None | Some("authorize") => Operation::Authorize, + Some("whoami") => Operation::WhoAmI, + Some("current-token") => Operation::CurrentToken, + Some("token-exchange") => Operation::TokenExchange, + Some("ssh-login") => Operation::SshLogin, + Some(other) => { + eprintln!("unknown operation {other}"); + std::process::exit(2); + } + }; let creds = ProcCredentials::current(); - let res = AuthorizeAction::build() + let res = AuthorizeAction::build(operation) .with_message(|_| Ok(PlatformString::new_with_default("authz prompt"))) .with_uid(|_| Ok("static".to_string())) .with_success_timeout(Duration::from_hours(1)) diff --git a/ak-platform-authz/src/grpc.rs b/ak-platform-authz/src/grpc.rs index a324f57ab..7fd7c14a1 100644 --- a/ak-platform-authz/src/grpc.rs +++ b/ak-platform-authz/src/grpc.rs @@ -1,13 +1,14 @@ use ak_platform::net::server::creds::ProcCredentials; -use crate::{AuthorizeAction, AuthorizeActionBuilder}; +use crate::{AuthorizeAction, AuthorizeActionBuilder, Operation}; pub trait AuthPeer { - fn auth_peer(&self) -> AuthorizeActionBuilder; + fn auth_peer(&self, operation: Operation) -> AuthorizeActionBuilder; } impl AuthPeer for tonic::Request { - fn auth_peer(&self) -> AuthorizeActionBuilder { - AuthorizeAction::build().with_creds(self.extensions().get::().cloned()) + fn auth_peer(&self, operation: Operation) -> AuthorizeActionBuilder { + AuthorizeAction::build(operation) + .with_creds(self.extensions().get::().cloned()) } } diff --git a/ak-platform-authz/src/lib.rs b/ak-platform-authz/src/lib.rs index ca4f9bdec..34a466a23 100644 --- a/ak-platform-authz/src/lib.rs +++ b/ak-platform-authz/src/lib.rs @@ -14,7 +14,24 @@ type UidFn = dyn (Fn(&ProcCredentials) -> Result) + Send + Sync; pub mod grpc; pub mod sys; +/// What a prompt asks the user to allow. On Linux each operation is its own polkit action, so +/// rules and retained authorizations apply to one operation at a time. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Operation { + /// Read the user's account information. + WhoAmI, + /// Hand out the agent's own session token. + CurrentToken, + /// Hand out a token for another application. + TokenExchange, + /// Log in to an SSH host. + SshLogin, + /// A generic approval for a service the caller names. + Authorize, +} + pub struct AuthorizeAction { + operation: Operation, message: Box, uid: Box, timeout_success: Duration, @@ -31,8 +48,15 @@ static LAST_AUTH_MAP: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); impl AuthorizeAction { - pub fn build() -> AuthorizeActionBuilder { - AuthorizeActionBuilder::default() + pub fn build(operation: Operation) -> AuthorizeActionBuilder { + AuthorizeActionBuilder { + operation, + message: None, + uid: None, + timeout_success: Duration::ZERO, + timeout_denied: Duration::ZERO, + creds: None, + } } pub fn timeout(&self, status: bool) -> Duration { @@ -42,7 +66,7 @@ impl AuthorizeAction { } } - #[tracing::instrument(skip(self), fields(uid))] + #[tracing::instrument(skip(self), fields(operation = ?self.operation, uid))] pub async fn prompt(self, creds: ProcCredentials) -> Result { let uid = (self.uid)(&creds) .wrap_err("failed to resolve authorization UID")? @@ -63,7 +87,7 @@ impl AuthorizeAction { .wrap_err("failed to build authorization message")? .clone(); tracing::trace!(uid, "Prompting for authz"); - let res = match sys::prompt(msg).await { + let res = match sys::prompt(self.operation, msg).await { Ok(r) => r, Err(e) => { tracing::trace!("error during authz: {e:?}"); @@ -102,8 +126,8 @@ impl AuthorizeAction { } } -#[derive(Default)] pub struct AuthorizeActionBuilder { + operation: Operation, message: Option>, uid: Option>, timeout_success: Duration, @@ -146,6 +170,7 @@ impl AuthorizeActionBuilder { bail!("Missing uid function"); }; Ok(AuthorizeAction { + operation: self.operation, message: m, uid: u, timeout_success: self.timeout_success, diff --git a/ak-platform-authz/src/sys/linux.rs b/ak-platform-authz/src/sys/linux.rs index 9e3991459..4434fe696 100644 --- a/ak-platform-authz/src/sys/linux.rs +++ b/ak-platform-authz/src/sys/linux.rs @@ -3,6 +3,20 @@ use eyre::{Result, WrapErr}; use std::collections::HashMap; use zbus::zvariant::OwnedValue; +use crate::Operation; + +/// The polkit action for each operation, as declared (and documented for rule authors) in +/// `vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy`. +fn action_id(operation: Operation) -> &'static str { + match operation { + Operation::WhoAmI => "io.goauthentik.platform.whoami", + Operation::CurrentToken => "io.goauthentik.platform.current-token", + Operation::TokenExchange => "io.goauthentik.platform.token-exchange", + Operation::SshLogin => "io.goauthentik.platform.ssh-login", + Operation::Authorize => "io.goauthentik.platform.authorize", + } +} + #[zbus::proxy( interface = "org.freedesktop.PolicyKit1.Authority", default_service = "org.freedesktop.PolicyKit1", @@ -32,7 +46,7 @@ fn proc_start_time() -> Option { fields.get(19)?.parse().ok() } -pub async fn prompt(_msg: PlatformString) -> Result { +pub async fn prompt(operation: Operation, _msg: PlatformString) -> Result { let conn = match zbus::Connection::system().await { Ok(c) => c, Err(e) => { @@ -61,7 +75,9 @@ pub async fn prompt(_msg: PlatformString) -> Result { let (is_authorized, _, _) = proxy .check_authorization( ("unix-process".to_string(), subject_details), - "io.goauthentik.platform.authorize", + action_id(operation), + // polkitd rejects the whole check if anyone but root or an action owner passes + // details, so nothing per request (like `polkit.message`) can be sent from here. HashMap::new(), 1u32, // ALLOW_USER_INTERACTION "", @@ -71,3 +87,37 @@ pub async fn prompt(_msg: PlatformString) -> Result { Ok(is_authorized) } + +#[cfg(test)] +mod tests { + use super::*; + + const POLICY: &str = include_str!( + "../../../vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy" + ); + + const OPERATIONS: [Operation; 5] = [ + Operation::WhoAmI, + Operation::CurrentToken, + Operation::TokenExchange, + Operation::SshLogin, + Operation::Authorize, + ]; + + #[test] + fn every_action_is_declared() { + for operation in OPERATIONS { + let id = action_id(operation); + assert!( + POLICY.contains(&format!("")), + "{id} is not declared in the policy file" + ); + } + } + + #[test] + fn operations_have_their_own_action() { + let ids: std::collections::HashSet<_> = OPERATIONS.into_iter().map(action_id).collect(); + assert_eq!(ids.len(), OPERATIONS.len()); + } +} diff --git a/ak-platform-authz/src/sys/macos.rs b/ak-platform-authz/src/sys/macos.rs index 44b77e22c..0ded4b330 100644 --- a/ak-platform-authz/src/sys/macos.rs +++ b/ak-platform-authz/src/sys/macos.rs @@ -1,7 +1,9 @@ use eyre::{Error, Result, WrapErr}; use localauthentication::{async_api::AsyncContextExt, prelude::*}; -pub async fn prompt(msg: String) -> Result { +use crate::Operation; + +pub async fn prompt(_operation: Operation, msg: String) -> Result { let (tx, rx) = tokio::sync::oneshot::channel(); std::thread::Builder::new() diff --git a/ak-platform-authz/src/sys/mod.rs b/ak-platform-authz/src/sys/mod.rs index fae06841f..74ce8f7ae 100644 --- a/ak-platform-authz/src/sys/mod.rs +++ b/ak-platform-authz/src/sys/mod.rs @@ -1,6 +1,8 @@ use ak_platform::string::PlatformString; use eyre::Result; +use crate::Operation; + #[cfg(target_os = "macos")] pub mod macos; @@ -10,11 +12,11 @@ pub mod linux; #[cfg(target_os = "windows")] pub mod windows; -pub async fn prompt(msg: PlatformString) -> Result { +pub async fn prompt(operation: Operation, msg: PlatformString) -> Result { #[cfg(target_os = "macos")] - return macos::prompt(msg.for_current()).await; + return macos::prompt(operation, msg.for_current()).await; #[cfg(target_os = "linux")] - return linux::prompt(msg).await; + return linux::prompt(operation, msg).await; #[cfg(target_os = "windows")] - return windows::prompt(msg).await; + return windows::prompt(operation, msg).await; } diff --git a/ak-platform-authz/src/sys/windows.rs b/ak-platform-authz/src/sys/windows.rs index 198c568a7..e51b42c1c 100644 --- a/ak-platform-authz/src/sys/windows.rs +++ b/ak-platform-authz/src/sys/windows.rs @@ -8,7 +8,9 @@ use windows::{ core::HSTRING, }; -pub async fn prompt(msg: PlatformString) -> Result { +use crate::Operation; + +pub async fn prompt(_operation: Operation, msg: PlatformString) -> Result { let hmsg = HSTRING::from(msg.for_current()); match UserConsentVerifier::CheckAvailabilityAsync()?.await? { UserConsentVerifierAvailability::Available => (), diff --git a/vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy b/vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy index 9975cb19c..bd4ce6151 100644 --- a/vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy +++ b/vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy @@ -1,9 +1,70 @@ + authentik Platform Agent https://goauthentik.io + + Read your authentik account information. + Authentication is required to let a program read your authentik account information. + + auth_self + auth_self + auth_self_keep + + + + Give a program your authentik session token. + Authentication is required to give a program your authentik session token. + + auth_self + auth_self + auth_self_keep + + + + Give a program a token for another application. + Authentication is required to give a program a token for another application in your authentik account. + + auth_self + auth_self + auth_self_keep + + + + Log in to an SSH host with your authentik account. + Authentication is required to log in to an SSH host with your authentik account. + + auth_self + auth_self + auth_self_keep + + Authorize actions on behalf of your authentik account. Authorize access to your account. From 0edb6775ad875bd303db5c8caa784e322e63c9e3 Mon Sep 17 00:00:00 2001 From: Emil Burzo Date: Fri, 23 Oct 2026 15:11:10 +0300 Subject: [PATCH 2/3] fix conflicts --- ak-platform-authz/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ak-platform-authz/src/lib.rs b/ak-platform-authz/src/lib.rs index b4aa994ab..dc90de3ee 100644 --- a/ak-platform-authz/src/lib.rs +++ b/ak-platform-authz/src/lib.rs @@ -101,6 +101,8 @@ impl AuthorizeAction { timeout_success: Duration::ZERO, timeout_denied: Duration::ZERO, creds: None, + action: "", + target: String::new(), } } @@ -279,7 +281,7 @@ mod tests { fn history_is_bounded_and_newest_first() -> Result<()> { let creds = ProcCredentials::current(); for i in 0..=HISTORY_SIZE { - AuthorizeAction::build() + AuthorizeAction::build(Operation::Authorize) .with_message(|_| Ok(PlatformString::new())) .with_uid(|_| Ok(String::new())) .with_target("test", i.to_string()) From 95a88b5ac84122af51b159403f640bdad858a24d Mon Sep 17 00:00:00 2001 From: Emil Burzo Date: Fri, 23 Oct 2026 15:54:04 +0300 Subject: [PATCH 3/3] less silly comments --- ak-platform-authz/src/lib.rs | 2 -- ak-platform-authz/src/sys/linux.rs | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ak-platform-authz/src/lib.rs b/ak-platform-authz/src/lib.rs index dc90de3ee..0fa9bbc4c 100644 --- a/ak-platform-authz/src/lib.rs +++ b/ak-platform-authz/src/lib.rs @@ -15,8 +15,6 @@ type UidFn = dyn (Fn(&ProcCredentials) -> Result) + Send + Sync; pub mod grpc; pub mod sys; -/// What a prompt asks the user to allow. On Linux each operation is its own polkit action, so -/// rules and retained authorizations apply to one operation at a time. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Operation { /// Read the user's account information. diff --git a/ak-platform-authz/src/sys/linux.rs b/ak-platform-authz/src/sys/linux.rs index 4434fe696..65434a9e4 100644 --- a/ak-platform-authz/src/sys/linux.rs +++ b/ak-platform-authz/src/sys/linux.rs @@ -5,8 +5,7 @@ use zbus::zvariant::OwnedValue; use crate::Operation; -/// The polkit action for each operation, as declared (and documented for rule authors) in -/// `vpkg/linux/agent/usr/share/polkit-1/actions/io.goauthentik.platform.policy`. +/// The polkit action for each operation fn action_id(operation: Operation) -> &'static str { match operation { Operation::WhoAmI => "io.goauthentik.platform.whoami", @@ -76,8 +75,7 @@ pub async fn prompt(operation: Operation, _msg: PlatformString) -> Result .check_authorization( ("unix-process".to_string(), subject_details), action_id(operation), - // polkitd rejects the whole check if anyone but root or an action owner passes - // details, so nothing per request (like `polkit.message`) can be sent from here. + // polkitd rejects the whole check if anyone but root passes details HashMap::new(), 1u32, // ALLOW_USER_INTERACTION "",