Skip to content

Commit

Permalink
Implement logon scripts for ensuring compliance
Browse files Browse the repository at this point in the history
Introduces first_time_logon_script and
logon_script options.

Signed-off-by: David Mulder <[email protected]>
  • Loading branch information
dmulder committed Oct 29, 2024
1 parent 3591d5f commit 6a6e846
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 14 deletions.
16 changes: 16 additions & 0 deletions platform/debian/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@
# when groups are removed from this list. You must remove them manually.
local_groups = users
#
# First time logon user script. This script will execute the first time a user
# logs in to the host. 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.
# first_time_logon_script =
# first_time_logon_token_scopes =
#
# 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.
# logon_script =
# logon_token_scopes =
#
# authority_host = login.microsoftonline.com
#
# The location of the cache database
Expand Down
22 changes: 22 additions & 0 deletions src/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,28 @@ impl HimmelblauConfig {
None => vec![],
}
}

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

pub fn get_first_time_logon_token_scopes(&self) -> Vec<String> {
match self.config.get("global", "first_time_logon_token_scopes") {
Some(scopes) => scopes.split(",").map(|s| s.to_string()).collect(),
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
194 changes: 191 additions & 3 deletions src/common/src/idprovider/himmelblau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::config::IdAttr;
use crate::constants::DEFAULT_GRAPH;
use crate::db::KeyStoreTxn;
use crate::idprovider::interface::tpm;
use crate::unix_proto::PamAuthRequest;
use crate::unix_proto::{AsyncTaskRequest, PamAuthRequest, TaskRequest};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use himmelblau::auth::{BrokerClientApplication, UserToken as UnixUserToken};
Expand All @@ -41,7 +41,10 @@ use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use std::time::SystemTime;
use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot;
use tokio::sync::{broadcast, RwLock};
use tokio::time;
use uuid::Uuid;

#[derive(Deserialize, Serialize)]
Expand Down Expand Up @@ -308,6 +311,7 @@ impl IdProvider for HimmelblauMultiProvider {
tpm: &mut tpm::BoxedDynTpm,
machine_key: &tpm::MachineKey,
shutdown_rx: &broadcast::Receiver<()>,
task_channel_tx: &Sender<AsyncTaskRequest>,
) -> Result<(AuthResult, AuthCacheAction), IdpError> {
match split_username(account_id) {
Some((_sam, domain)) => {
Expand All @@ -323,6 +327,7 @@ impl IdProvider for HimmelblauMultiProvider {
tpm,
machine_key,
shutdown_rx,
task_channel_tx,
)
.await
}
Expand Down Expand Up @@ -371,6 +376,7 @@ impl IdProvider for HimmelblauMultiProvider {
tpm: &mut tpm::BoxedDynTpm,
machine_key: &tpm::MachineKey,
online_at_init: bool,
task_channel_tx: &Sender<AsyncTaskRequest>,
) -> Result<AuthResult, IdpError> {
match split_username(account_id) {
Some((_sam, domain)) => {
Expand All @@ -387,6 +393,7 @@ impl IdProvider for HimmelblauMultiProvider {
tpm,
machine_key,
online_at_init,
task_channel_tx,
)
.await
}
Expand Down Expand Up @@ -669,7 +676,76 @@ impl IdProvider for HimmelblauProvider {
tpm: &mut tpm::BoxedDynTpm,
machine_key: &tpm::MachineKey,
_shutdown_rx: &broadcast::Receiver<()>,
task_channel_tx: &Sender<AsyncTaskRequest>,
) -> Result<(AuthResult, AuthCacheAction), IdpError> {
macro_rules! execute_logon_script {
($token:ident) => {{
// Send a logon script request to the Task daemon
let cfg = self.config.read().await;
if let Some(_) = cfg.get_logon_script() {
let scopes = cfg.get_logon_token_scopes();
let str_scopes: Vec<&str> = scopes.iter()
.map(|s| {
let slice = s.as_str();
slice
})
.collect();
let script_token = self
.client
.write()
.await
.acquire_token_by_refresh_token(
&$token.refresh_token,
str_scopes,
Some("https://graph.microsoft.com".to_string()),
tpm,
machine_key,
)
.await;
match script_token {
Ok(script_token) => match &script_token.access_token {
Some(access_token) => {
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_millis(1000),
rx,
)
.await
{
Ok(Ok(_)) => {}
_ => {
error!("Execution of logon script failed");
}
}
}
Err(e) => {
error!("Execution of logon script failed: {:?}", e);
}
}
}
None => error!("Failed fetching access token for logon script"),
},
Err(e) => error!("Failed fetching access token for logon script: {:?}", e),
}
}
}}
}
macro_rules! enroll_and_obtain_enrolled_token {
($token:ident) => {{
if !self.is_domain_joined(keystore).await {
Expand All @@ -680,6 +756,71 @@ impl IdProvider for HimmelblauProvider {
error!("Failed to join domain: {:?}", e);
IdpError::BadRequest
})?;

// Send a first time logon script request to the Task daemon
let cfg = self.config.read().await;
if let Some(_) = cfg.get_first_time_logon_script() {
let scopes = cfg.get_first_time_logon_token_scopes();
let str_scopes: Vec<&str> = scopes.iter()
.map(|s| {
let slice = s.as_str();
slice
})
.collect();
let script_token = self
.client
.write()
.await
.acquire_token_by_refresh_token(
&$token.refresh_token,
str_scopes,
Some("https://graph.microsoft.com".to_string()),
tpm,
machine_key,
)
.await;
match script_token {
Ok(script_token) => match &script_token.access_token {
Some(access_token) => {
let (tx, rx) = oneshot::channel();

match task_channel_tx
.send_timeout(
(
TaskRequest::FirstTimeLogonScript(
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_millis(1000),
rx,
)
.await
{
Ok(Ok(_)) => {}
_ => {
error!("Execution of first time logon script failed");
}
}
}
Err(e) => {
error!("Execution of first time logon script failed: {:?}", e);
}
}
}
None => error!("Failed fetching access token for first time logon script"),
},
Err(e) => error!("Failed fetching access token for first time logon script: {:?}", e),
}
}
}
let mtoken2 = self
.client
Expand Down Expand Up @@ -753,9 +894,10 @@ impl IdProvider for HimmelblauProvider {
})?;

match self.token_validate(account_id, &token).await {
Ok(AuthResult::Success { token }) => {
Ok(AuthResult::Success { token: token2 }) => {
debug!("Returning user token from successful Hello PIN authentication.");
Ok((AuthResult::Success { token }, AuthCacheAction::None))
execute_logon_script!(token);
Ok((AuthResult::Success { token: token2 }, AuthCacheAction::None))
}
/* This should never happen. It doesn't make sense to
* continue from a Pin auth. */
Expand Down Expand Up @@ -874,6 +1016,7 @@ impl IdProvider for HimmelblauProvider {
let token2 = enroll_and_obtain_enrolled_token!(token);
return match self.token_validate(account_id, &token2).await {
Ok(AuthResult::Success { token }) => {
execute_logon_script!(token2);
// STOP! If we just enrolled with an SFA token, then we
// need to bail out here and refuse Hello enrollment
// (we can't enroll in Hello with an SFA token).
Expand Down Expand Up @@ -941,6 +1084,7 @@ impl IdProvider for HimmelblauProvider {
let hello_enabled = self.config.read().await.get_enable_hello();
if !hello_enabled {
info!("Skipping Hello enrollment because it is disabled");
execute_logon_script!(token2);
return Ok((
AuthResult::Success { token: token3 },
AuthCacheAction::None,
Expand Down Expand Up @@ -1001,6 +1145,7 @@ impl IdProvider for HimmelblauProvider {
let hello_enabled = self.config.read().await.get_enable_hello();
if !hello_enabled {
info!("Skipping Hello enrollment because it is disabled");
execute_logon_script!(token2);
return Ok((
AuthResult::Success { token: token3 },
AuthCacheAction::None,
Expand Down Expand Up @@ -1060,7 +1205,49 @@ impl IdProvider for HimmelblauProvider {
tpm: &mut tpm::BoxedDynTpm,
machine_key: &tpm::MachineKey,
_online_at_init: bool,
task_channel_tx: &Sender<AsyncTaskRequest>,
) -> Result<AuthResult, IdpError> {
macro_rules! execute_logon_script {
() => {{
// Send a logon script request to the Task daemon
let cfg = self.config.read().await;
if let Some(_) = cfg.get_first_time_logon_script() {
let (tx, rx) = oneshot::channel();

match task_channel_tx
.send_timeout(
(
TaskRequest::LogonScript(
account_id.to_string(),
"".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_millis(1000),
rx,
)
.await
{
Ok(Ok(_)) => {}
_ => {
error!("Execution of logon script failed");
}
}
}
Err(e) => {
error!("Execution of logon script failed: {:?}", e);
}
}
}
}}
}
match (&cred_handler, pam_next_req) {
(_, PamAuthRequest::Pin { cred }) => {
let hello_tag = self.fetch_hello_key_tag(account_id);
Expand All @@ -1084,6 +1271,7 @@ impl IdProvider for HimmelblauProvider {
error!("{:?}", e);
IdpError::BadRequest
})?;
execute_logon_script!();
Ok(AuthResult::Success {
token: token.clone(),
})
Expand Down
5 changes: 4 additions & 1 deletion src/common/src/idprovider/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
*/

use crate::db::KeyStoreTxn;
use crate::unix_proto::{PamAuthRequest, PamAuthResponse};
use crate::unix_proto::{AsyncTaskRequest, PamAuthRequest, PamAuthResponse};
use async_trait::async_trait;
use himmelblau::{MFAAuthContinue, UserToken as UnixUserToken};
use serde::{Deserialize, Serialize};
use std::fmt;
use tokio::sync::broadcast;
use tokio::sync::mpsc::Sender;
use uuid::Uuid;

pub use kanidm_hsm_crypto as tpm;
Expand Down Expand Up @@ -207,6 +208,7 @@ pub trait IdProvider {
_tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey,
_shutdown_rx: &broadcast::Receiver<()>,
_task_channel_tx: &Sender<AsyncTaskRequest>,
) -> Result<(AuthResult, AuthCacheAction), IdpError>;

async fn unix_user_offline_auth_init<D: KeyStoreTxn + Send>(
Expand Down Expand Up @@ -245,6 +247,7 @@ pub trait IdProvider {
_tpm: &mut tpm::BoxedDynTpm,
_machine_key: &tpm::MachineKey,
_online_at_init: bool,
_task_channel_tx: &Sender<AsyncTaskRequest>,
) -> Result<AuthResult, IdpError>;

async fn unix_group_get(
Expand Down
Loading

0 comments on commit 6a6e846

Please sign in to comment.