Skip to content

Commit

Permalink
Merge pull request #273 from himmelblau-idm/dmulder/compliance_options
Browse files Browse the repository at this point in the history
Adding compliance options
  • Loading branch information
dmulder authored Nov 1, 2024
2 parents 2d42ac9 + 1d052d9 commit 1bc67e0
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 23 deletions.
17 changes: 17 additions & 0 deletions platform/debian/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@
# configuration.
# cn_name_mapping = true
#
# A comma seperated list of local groups that every Entra Id user should be a
# member of. For example, you may wish for all Entra Id users to be a member
# of the sudo group. WARNING: This setting will not REMOVE group member entries
# 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
19 changes: 19 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 @@ -453,6 +454,24 @@ impl HimmelblauConfig {
pub fn get_tenant_id(&self, domain: &str) -> Option<String> {
self.config.get(domain, "tenant_id")
}

pub fn get_local_groups(&self) -> Vec<String> {
match self.config.get("global", "local_groups") {
Some(val) => val.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
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
4 changes: 3 additions & 1 deletion src/common/src/unix_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ pub struct HomeDirectoryInfo {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TaskRequest {
HomeDirectory(HomeDirectoryInfo),
LocalGroups(String),
LogonScript(String, String),
}

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

Expand Down
17 changes: 17 additions & 0 deletions src/config/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@
# configuration.
# cn_name_mapping = true
#
# A comma seperated list of local groups that every Entra Id user should be a
# member of. For example, you may wish for all Entra Id users to be a member
# of the sudo group. WARNING: This setting will not REMOVE group member entries
# 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
161 changes: 143 additions & 18 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 All @@ -347,7 +433,7 @@ async fn handle_client(
Ok(Some(info)) => {
let (tx, rx) = oneshot::channel();

match task_channel_tx
let resp1 = match task_channel_tx
.send_timeout(
(TaskRequest::HomeDirectory(info), tx),
Duration::from_millis(100),
Expand Down Expand Up @@ -376,6 +462,44 @@ async fn handle_client(
// We could not submit the req. Move on!
ClientResponse::Error
}
};

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

let resp2 = match task_channel_tx
.send_timeout(
(TaskRequest::LocalGroups(account_id.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(_)) => {
debug!("Task completed, returning to pam ...");
ClientResponse::Ok
}
_ => {
// Timeout or other error.
ClientResponse::Error
}
}
}
Err(_) => {
// We could not submit the req. Move on!
ClientResponse::Error
}
};

match resp1 {
ClientResponse::Error => ClientResponse::Error,
_ => resp2,
}
}
_ => ClientResponse::Error,
Expand Down Expand Up @@ -815,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 @@ -859,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 @@ -993,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 @@ -1003,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
Loading

0 comments on commit 1bc67e0

Please sign in to comment.