diff --git a/ak-agent/src/grpc/agent_auth.rs b/ak-agent/src/grpc/agent_auth.rs index e75e6c3b5..7d04cc5f4 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}; @@ -127,7 +127,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() @@ -190,7 +190,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()?; @@ -253,7 +253,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() @@ -320,7 +320,7 @@ impl AgentAuth for AgentGRPCServer { let uid = inner.uid.clone(); request - .auth_peer() + .auth_peer(Operation::Authorize) .with_target("Login", inner.service.clone()) .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 77adf5108..b70876418 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 3df9763a6..0fa9bbc4c 100644 --- a/ak-platform-authz/src/lib.rs +++ b/ak-platform-authz/src/lib.rs @@ -15,7 +15,22 @@ type UidFn = dyn (Fn(&ProcCredentials) -> Result) + Send + Sync; pub mod grpc; pub mod sys; +#[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, @@ -76,8 +91,17 @@ pub fn history() -> Vec { } 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, + action: "", + target: String::new(), + } } pub fn timeout(&self, status: bool) -> Duration { @@ -87,7 +111,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")? @@ -110,7 +134,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:?}"); @@ -172,8 +196,8 @@ impl AuthorizeAction { } } -#[derive(Default)] pub struct AuthorizeActionBuilder { + operation: Operation, message: Option>, uid: Option>, timeout_success: Duration, @@ -224,6 +248,7 @@ impl AuthorizeActionBuilder { bail!("Missing uid function"); }; Ok(AuthorizeAction { + operation: self.operation, message: m, uid: u, timeout_success: self.timeout_success, @@ -254,7 +279,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()) diff --git a/ak-platform-authz/src/sys/linux.rs b/ak-platform-authz/src/sys/linux.rs index 9e3991459..65434a9e4 100644 --- a/ak-platform-authz/src/sys/linux.rs +++ b/ak-platform-authz/src/sys/linux.rs @@ -3,6 +3,19 @@ use eyre::{Result, WrapErr}; use std::collections::HashMap; use zbus::zvariant::OwnedValue; +use crate::Operation; + +/// The polkit action for each operation +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 +45,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 +74,8 @@ 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 passes details HashMap::new(), 1u32, // ALLOW_USER_INTERACTION "", @@ -71,3 +85,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.