Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions ak-agent/src/grpc/agent_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()?;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)))
Expand Down
4 changes: 2 additions & 2 deletions ak-agent/src/ssh/txn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 13 additions & 2 deletions ak-platform-authz/examples/authz_prompt.rs
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
9 changes: 5 additions & 4 deletions ak-platform-authz/src/grpc.rs
Original file line number Diff line number Diff line change
@@ -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<T> AuthPeer for tonic::Request<T> {
fn auth_peer(&self) -> AuthorizeActionBuilder {
AuthorizeAction::build().with_creds(self.extensions().get::<ProcCredentials>().cloned())
fn auth_peer(&self, operation: Operation) -> AuthorizeActionBuilder {
AuthorizeAction::build(operation)
.with_creds(self.extensions().get::<ProcCredentials>().cloned())
}
}
37 changes: 31 additions & 6 deletions ak-platform-authz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,22 @@ type UidFn = dyn (Fn(&ProcCredentials) -> Result<String>) + 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<MessageFn>,
uid: Box<UidFn>,
timeout_success: Duration,
Expand Down Expand Up @@ -76,8 +91,17 @@ pub fn history() -> Vec<AuthRecord> {
}

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 {
Expand All @@ -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<bool> {
let uid = (self.uid)(&creds)
.wrap_err("failed to resolve authorization UID")?
Expand All @@ -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:?}");
Expand Down Expand Up @@ -172,8 +196,8 @@ impl AuthorizeAction {
}
}

#[derive(Default)]
pub struct AuthorizeActionBuilder {
operation: Operation,
message: Option<Box<MessageFn>>,
uid: Option<Box<UidFn>>,
timeout_success: Duration,
Expand Down Expand Up @@ -224,6 +248,7 @@ impl AuthorizeActionBuilder {
bail!("Missing uid function");
};
Ok(AuthorizeAction {
operation: self.operation,
message: m,
uid: u,
timeout_success: self.timeout_success,
Expand Down Expand Up @@ -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())
Expand Down
52 changes: 50 additions & 2 deletions ak-platform-authz/src/sys/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -32,7 +45,7 @@ fn proc_start_time() -> Option<u64> {
fields.get(19)?.parse().ok()
}

pub async fn prompt(_msg: PlatformString) -> Result<bool> {
pub async fn prompt(operation: Operation, _msg: PlatformString) -> Result<bool> {
let conn = match zbus::Connection::system().await {
Ok(c) => c,
Err(e) => {
Expand Down Expand Up @@ -61,7 +74,8 @@ pub async fn prompt(_msg: PlatformString) -> Result<bool> {
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
"",
Expand All @@ -71,3 +85,37 @@ pub async fn prompt(_msg: PlatformString) -> Result<bool> {

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!("<action id=\"{id}\">")),
"{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());
}
}
4 changes: 3 additions & 1 deletion ak-platform-authz/src/sys/macos.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use eyre::{Error, Result, WrapErr};
use localauthentication::{async_api::AsyncContextExt, prelude::*};

pub async fn prompt(msg: String) -> Result<bool> {
use crate::Operation;

pub async fn prompt(_operation: Operation, msg: String) -> Result<bool> {
let (tx, rx) = tokio::sync::oneshot::channel();

std::thread::Builder::new()
Expand Down
10 changes: 6 additions & 4 deletions ak-platform-authz/src/sys/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use ak_platform::string::PlatformString;
use eyre::Result;

use crate::Operation;

#[cfg(target_os = "macos")]
pub mod macos;

Expand All @@ -10,11 +12,11 @@ pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;

pub async fn prompt(msg: PlatformString) -> Result<bool> {
pub async fn prompt(operation: Operation, msg: PlatformString) -> Result<bool> {
#[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;
}
4 changes: 3 additions & 1 deletion ak-platform-authz/src/sys/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use windows::{
core::HSTRING,
};

pub async fn prompt(msg: PlatformString) -> Result<bool> {
use crate::Operation;

pub async fn prompt(_operation: Operation, msg: PlatformString) -> Result<bool> {
let hmsg = HSTRING::from(msg.for_current());
match UserConsentVerifier::CheckAvailabilityAsync()?.await? {
UserConsentVerifierAvailability::Available => (),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://specifications.freedesktop.org/PolicyKit/1.0/policyconfig.dtd">
<!--
The authentik agent checks each request against one of the actions below, picked by what
the request would hand out, so polkit rules can allow one kind of request and keep prompting
for the rest. An approval kept by auth_self_keep (about 5 minutes) only covers further
requests for the same action, from any program.

What a rule can rely on: action.id is chosen by the agent for the request, and subject.user
is the user the agent runs as. The subject is the agent process itself, not the program
that asked it, and polkit doesn't accept details from the agent. So a rule can't tell which
program asked, or which application a token is for: allowing token-exchange lets every
program running as that user get tokens for any application that user can access.

Example: let alice's command line tools, such as `ak auth claude`, get tokens without a
prompt, in /etc/polkit-1/rules.d/50-authentik-agent.rules:

polkit.addRule(function (action, subject) {
if (action.id == "io.goauthentik.platform.token-exchange" &&
subject.user == "alice" && subject.local && subject.active) {
return polkit.Result.YES;
}
});

Everything else keeps prompting, in particular current-token, which hands out the agent's
own session token.
-->
<policyconfig>
<vendor>authentik Platform Agent</vendor>
<vendor_url>https://goauthentik.io</vendor_url>
<action id="io.goauthentik.platform.whoami">
<description>Read your authentik account information.</description>
<message>Authentication is required to let a program read your authentik account information.</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self_keep</allow_active>
</defaults>
</action>
<action id="io.goauthentik.platform.current-token">
<description>Give a program your authentik session token.</description>
<message>Authentication is required to give a program your authentik session token.</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self_keep</allow_active>
</defaults>
</action>
<action id="io.goauthentik.platform.token-exchange">
<description>Give a program a token for another application.</description>
<message>Authentication is required to give a program a token for another application in your authentik account.</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self_keep</allow_active>
</defaults>
</action>
<action id="io.goauthentik.platform.ssh-login">
<description>Log in to an SSH host with your authentik account.</description>
<message>Authentication is required to log in to an SSH host with your authentik account.</message>
<defaults>
<allow_any>auth_self</allow_any>
<allow_inactive>auth_self</allow_inactive>
<allow_active>auth_self_keep</allow_active>
</defaults>
</action>
<action id="io.goauthentik.platform.authorize">
<description>Authorize actions on behalf of your authentik account.</description>
<message>Authorize access to your account.</message>
Expand Down
Loading