Skip to content

Commit

Permalink
Implement logon script for ensuring compliance
Browse files Browse the repository at this point in the history
Introduces a logon_script option.

Signed-off-by: David Mulder <[email protected]>
  • Loading branch information
dmulder committed Nov 1, 2024
1 parent 3591d5f commit 1d052d9
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 23 deletions.
11 changes: 11 additions & 0 deletions platform/debian/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
# when groups are removed from this list. You must remove them manually.
local_groups = users
#
# Logon user script. This script will execute every time a user logs on. Two
# environment variables are set: USERNAME, and ACCESS_TOKEN. The ACCESS_TOKEN
# environment variable is an access token for the MS graph. The token scope
# config option sets the comma separated scopes that should be requested for
# the ACCESS_TOKEN. ACCESS_TOKEN will be empty during offline logon. The return
# code of the script determines how the authentication proceeds. 0 is success,
# 1 is a soft failure and authentication will proceed, while 2 is a hard
# failure causing authentication to fail.
# logon_script =
# logon_token_scopes =
#
# authority_host = login.microsoftonline.com
#
# The location of the cache database
Expand Down
12 changes: 12 additions & 0 deletions src/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub fn split_username(username: &str) -> Option<(&str, &str)> {
None
}

#[derive(Clone)]
pub struct HimmelblauConfig {
config: Ini,
filename: String,
Expand Down Expand Up @@ -460,6 +461,17 @@ impl HimmelblauConfig {
None => vec![],
}
}

pub fn get_logon_script(&self) -> Option<String> {
self.config.get("global", "logon_script")
}

pub fn get_logon_token_scopes(&self) -> Vec<String> {
match self.config.get("global", "logon_token_scopes") {
Some(scopes) => scopes.split(",").map(|s| s.to_string()).collect(),
None => vec![],
}
}
}

impl fmt::Debug for HimmelblauConfig {
Expand Down
6 changes: 3 additions & 3 deletions src/common/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub enum AuthSession {
/// when they need to stop.
shutdown_rx: broadcast::Receiver<()>,
},
Success,
Success(String),
Denied,
}

Expand Down Expand Up @@ -1167,7 +1167,7 @@ where
warn!("Unable to proceed with offline auth, no token available");
Err(IdpError::NotFound)
}
(&mut AuthSession::Success, _) | (&mut AuthSession::Denied, _) => {
(&mut AuthSession::Success(_), _) | (&mut AuthSession::Denied, _) => {
Err(IdpError::BadRequest)
}
};
Expand All @@ -1184,7 +1184,7 @@ where
} else {
debug!("provider authentication success.");
self.set_cache_usertoken(&mut token).await?;
*auth_session = AuthSession::Success;
*auth_session = AuthSession::Success(token.spn);

Ok(PamAuthResponse::Success)
}
Expand Down
3 changes: 2 additions & 1 deletion src/common/src/unix_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,12 @@ pub struct HomeDirectoryInfo {
pub enum TaskRequest {
HomeDirectory(HomeDirectoryInfo),
LocalGroups(String),
LogonScript(String, String),
}

#[derive(Serialize, Deserialize, Debug)]
pub enum TaskResponse {
Success,
Success(i32),
Error(String),
}

Expand Down
11 changes: 11 additions & 0 deletions src/config/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
# when groups are removed from this list. You must remove them manually.
# local_groups =
#
# Logon user script. This script will execute every time a user logs on. Two
# environment variables are set: USERNAME, and ACCESS_TOKEN. The ACCESS_TOKEN
# environment variable is an access token for the MS graph. The token scope
# config option sets the comma separated scopes that should be requested for
# the ACCESS_TOKEN. ACCESS_TOKEN will be empty during offline logon. The return
# code of the script determines how the authentication proceeds. 0 is success,
# 1 is a soft failure and authentication will proceed, while 2 is a hard
# failure causing authentication to fail.
# logon_script =
# logon_token_scopes =
#
# authority_host = login.microsoftonline.com
#
# The location of the cache database
Expand Down
121 changes: 104 additions & 17 deletions src/daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ use himmelblau_unix_common::config::HimmelblauConfig;
use himmelblau_unix_common::constants::DEFAULT_CONFIG_PATH;
use himmelblau_unix_common::db::{Cache, CacheTxn, Db};
use himmelblau_unix_common::idprovider::himmelblau::HimmelblauMultiProvider;
use himmelblau_unix_common::resolver::Resolver;
use himmelblau_unix_common::idprovider::interface::Id;
use himmelblau_unix_common::resolver::{AuthSession, Resolver};
use himmelblau_unix_common::unix_config::{HsmType, UidAttr};
use himmelblau_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd};
use himmelblau_unix_common::unix_proto::{
ClientRequest, ClientResponse, TaskRequest, TaskResponse,
ClientRequest, ClientResponse, PamAuthResponse, TaskRequest, TaskResponse,
};

use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
Expand All @@ -68,7 +69,7 @@ use identity_dbus_broker::himmelblau_broker_serve;

//=== the codec

type AsyncTaskRequest = (TaskRequest, oneshot::Sender<()>);
type AsyncTaskRequest = (TaskRequest, oneshot::Sender<i32>);

#[derive(Default)]
struct ClientCodec;
Expand Down Expand Up @@ -184,11 +185,11 @@ async fn handle_task_client(
}

match reqs.next().await {
Some(Ok(TaskResponse::Success)) => {
Some(Ok(TaskResponse::Success(status))) => {
debug!("Task was acknowledged and completed.");
// Send a result back via the one-shot
// Ignore if it fails.
let _ = v.1.send(());
let _ = v.1.send(status);
}
other => {
error!("Error -> {:?}", other);
Expand All @@ -202,6 +203,7 @@ async fn handle_client(
sock: UnixStream,
cachelayer: Arc<Resolver<HimmelblauMultiProvider>>,
task_channel_tx: &Sender<AsyncTaskRequest>,
cfg: HimmelblauConfig,
) -> Result<(), Box<dyn Error>> {
debug!("Accepted connection");

Expand Down Expand Up @@ -319,11 +321,95 @@ async fn handle_client(
ClientRequest::PamAuthenticateStep(pam_next_req) => {
debug!("pam authenticate step");
match &mut pam_auth_session_state {
Some(auth_session) => cachelayer
.pam_account_authenticate_step(auth_session, pam_next_req)
.await
.map(|pam_auth_response| pam_auth_response.into())
.unwrap_or(ClientResponse::Error),
Some(auth_session) => {
match cachelayer
.pam_account_authenticate_step(auth_session, pam_next_req)
.await
.map(|pam_auth_response| pam_auth_response.into())
.unwrap_or(ClientResponse::Error)
{
ClientResponse::PamAuthenticateStepResponse(resp) => {
macro_rules! ret {
() => {
ClientResponse::PamAuthenticateStepResponse(resp)
};
}
match auth_session {
AuthSession::Success(account_id) => {
match resp {
PamAuthResponse::Success => {
if cfg.get_logon_script().is_some() {
let scopes = cfg.get_logon_token_scopes();
let access_token = match cachelayer
.get_user_accesstoken(
Id::Name(account_id.to_string()),
scopes,
)
.await
{
Some(token) => token
.access_token
.clone()
.unwrap_or("".to_string()),
None => "".to_string(),
};

let (tx, rx) = oneshot::channel();

match task_channel_tx
.send_timeout(
(
TaskRequest::LogonScript(
account_id.to_string(),
access_token.to_string(),
),
tx,
),
Duration::from_millis(100),
)
.await
{
Ok(()) => {
// Now wait for the other end OR timeout.
match time::timeout_at(
time::Instant::now()
+ Duration::from_secs(60),
rx,
)
.await
{
Ok(Ok(status)) => {
if status == 2 {
debug!("Authentication was explicitly denied by the logon script");
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Denied)
} else {
ret!()
}
}
_ => {
error!("Execution of logon script failed");
ret!()
}
}
}
Err(e) => {
error!("Execution of logon script failed: {:?}", e);
ret!()
}
}
} else {
ret!()
}
}
_ => ret!(),
}
}
_ => ret!(),
}
}
other => other,
}
}
None => {
warn!("Attempt to continue auth session while current session is inactive");
ClientResponse::Error
Expand Down Expand Up @@ -853,6 +939,12 @@ async fn main() -> ExitCode {
return ExitCode::FAILURE
}

// Setup the tasks socket first.
let (task_channel_tx, mut task_channel_rx) = channel(16);
let task_channel_tx = Arc::new(task_channel_tx);

let task_channel_tx_cln = task_channel_tx.clone();

let cl_inner = match Resolver::new(
db,
idprovider,
Expand Down Expand Up @@ -897,12 +989,6 @@ async fn main() -> ExitCode {
return ExitCode::FAILURE
}

// Setup the tasks socket first.
let (task_channel_tx, mut task_channel_rx) = channel(16);
let task_channel_tx = Arc::new(task_channel_tx);

let task_channel_tx_cln = task_channel_tx.clone();

// Start to build the worker tasks
let (broadcast_tx, mut broadcast_rx) = broadcast::channel(4);
let mut c_broadcast_rx = broadcast_tx.subscribe();
Expand Down Expand Up @@ -1031,6 +1117,7 @@ async fn main() -> ExitCode {
let task_a = tokio::spawn(async move {
loop {
let tc_tx = task_channel_tx_cln.clone();
let cfg_h = cfg.clone();

tokio::select! {
_ = broadcast_rx.recv() => {
Expand All @@ -1041,7 +1128,7 @@ async fn main() -> ExitCode {
Ok((socket, _addr)) => {
let cachelayer_ref = cachelayer.clone();
tokio::spawn(async move {
if let Err(e) = handle_client(socket, cachelayer_ref.clone(), &tc_tx).await
if let Err(e) = handle_client(socket, cachelayer_ref.clone(), &tc_tx, cfg_h).await
{
error!("handle_client error occurred; error = {:?}", e);
}
Expand Down
51 changes: 49 additions & 2 deletions src/daemon/src/tasks_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::ExitCode;
use std::str;
use std::time::Duration;
use std::{fs, io};

Expand Down Expand Up @@ -261,6 +262,40 @@ fn add_user_to_group(account_id: &str, local_group: &str) {
}
}

fn execute_user_script(account_id: &str, script: &str, access_token: &str) -> i32 {
match Command::new("sh")
.arg("-c")
.arg(script)
.env("USERNAME", account_id)
.env("ACCESS_TOKEN", access_token)
.output()
{
Ok(res) => {
if !res.status.success() {
let stdout = str::from_utf8(&res.stdout).unwrap_or("Invalid UTF-8 in stdout");
let stderr = str::from_utf8(&res.stderr).unwrap_or("Invalid UTF-8 in stderr");
error!(
"Failed to execute script '{}':\nstdout: {}\nstderr: {}",
script, stdout, stderr
);
}

// If we don't get a status code, make assumptions
if res.status.success() {
res.status.code().unwrap_or(0)
} else {
res.status.code().unwrap_or(2)
}
}
Err(e) => {
error!("Failed to execute script '{}': {:?}", script, e);
// If the script fails to execute at all, we assume this is a hard
// failure and terminate the authentication attempt.
2
}
}
}

async fn handle_tasks(stream: UnixStream, cfg: &HimmelblauConfig) {
let mut reqs = Framed::new(stream, TaskCodec::new());

Expand All @@ -276,7 +311,7 @@ async fn handle_tasks(stream: UnixStream, cfg: &HimmelblauConfig) {
cfg.get_use_etc_skel(),
cfg.get_selinux(),
) {
Ok(()) => TaskResponse::Success,
Ok(()) => TaskResponse::Success(0),
Err(msg) => TaskResponse::Error(msg),
};

Expand All @@ -294,7 +329,19 @@ async fn handle_tasks(stream: UnixStream, cfg: &HimmelblauConfig) {
}

// Always indicate success here
if let Err(e) = reqs.send(TaskResponse::Success).await {
if let Err(e) = reqs.send(TaskResponse::Success(0)).await {
error!("Error -> {:?}", e);
return;
}
}
Some(Ok(TaskRequest::LogonScript(account_id, access_token))) => {
let mut status = 0;
if let Some(script) = cfg.get_logon_script() {
status = execute_user_script(&account_id, &script, &access_token);
}

// Indicate the status response
if let Err(e) = reqs.send(TaskResponse::Success(status)).await {
error!("Error -> {:?}", e);
return;
}
Expand Down

0 comments on commit 1d052d9

Please sign in to comment.