Skip to content

Commit

Permalink
Implement logon name script mapping
Browse files Browse the repository at this point in the history
Signed-off-by: David Mulder <[email protected]>
  • Loading branch information
dmulder committed Nov 25, 2024
1 parent fa305c2 commit a84b57f
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 37 deletions.
8 changes: 8 additions & 0 deletions platform/debian/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ local_groups = users
# logon_script =
# logon_token_scopes =
#
# In some cases, mapping the UPN to the CN may be impractical. The following
# option executes the specifed filename (any executable), which MUST accept
# a single argument which will be EITHER the upn, or the mapped name. The
# executable MUST print a single response to stdout, which will be either the
# mapped name, or the upn, respectively. This executable must map the name
# bi-directionally.
# name_mapping_script =
#
# authority_host = login.microsoftonline.com
#
# The location of the cache database
Expand Down
115 changes: 115 additions & 0 deletions src/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::unix_passwd::parse_etc_passwd;
use configparser::ini::Ini;
use std::fmt;
use std::fs::File;
use std::io::Error;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, error};

use crate::constants::{
Expand Down Expand Up @@ -476,10 +480,121 @@ impl HimmelblauConfig {
None => vec![],
}
}

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

pub fn map_upn_to_name(&self, account_id: &str) -> String {
map_upn_to_name(
account_id,
&self.get_name_mapping_script(),
self.get_cn_name_mapping(),
&self.get_configured_domains(),
)
}

pub fn map_name_to_upn(&self, account_id: &str) -> String {
map_name_to_upn(
account_id,
&self.get_name_mapping_script(),
self.get_cn_name_mapping(),
&self.get_configured_domains(),
)
}
}

impl fmt::Debug for HimmelblauConfig {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self.config)
}
}

// This function maps a upn to a local username. If cn name mapping is enabled,
// this will map to the CN. Otherwise it will attempt to map using the name
// mapping script. If no name mapping is enabled, it will respond with the
// supplied UPN (no name mapping).
pub fn map_upn_to_name(
account_id: &str,
name_mapping_script: &Option<String>,
cn_name_mapping: bool,
domains: &[String],
) -> String {
// The name mapping script is expected to convert the input name to a UPN
// if a name is supplied, or to a name if the UPN is supplied.
if let Some(name_mapping_script) = &name_mapping_script {
let output = Command::new(name_mapping_script).arg(account_id).output();

match output {
Ok(output) => {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
} else {
eprintln!("Script execution failed with error: {:?}", output.status);
}
}
Err(e) => {
eprintln!("Failed to execute script: {}", e);
}
}
}
if cn_name_mapping && account_id.contains('@') && !domains.is_empty() {
if let Some((cn, domain)) = split_username(account_id) {
// We can only name map the default domain
if domain == domains[0] {
return cn.to_string();
}
}
}
account_id.to_string()
}

// This function attempts to convert a username to a valid UPN. On failure it
// will leave the name as-is, and respond with the original input. Himmelblau
// will reject the authentication attempt if the username isn't a valid UPN.
pub fn map_name_to_upn(
account_id: &str,
name_mapping_script: &Option<String>,
cn_name_mapping: bool,
domains: &[String],
) -> String {
// Make sure this account_id isn't a local user
let mut contents = vec![];
if let Ok(mut file) = File::open("/etc/passwd") {
let _ = file.read_to_end(&mut contents);
}
let local_users = parse_etc_passwd(contents.as_slice()).unwrap_or_default();
if local_users
.into_iter()
.map(|u| u.name.to_string())
.collect::<Vec<String>>()
.contains(&account_id.to_string())
{
return account_id.to_string();
}

// The name mapping script is expected to convert the input name to a UPN
// if a name is supplied, or to a name if the UPN is supplied.
if !account_id.contains('@') {
if let Some(name_mapping_script) = &name_mapping_script {
let output = Command::new(name_mapping_script).arg(account_id).output();

match output {
Ok(output) => {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
} else {
eprintln!("Script execution failed with error: {:?}", output.status);
}
}
Err(e) => {
eprintln!("Failed to execute script: {}", e);
}
}
}
}
if cn_name_mapping && !account_id.contains('@') && !domains.is_empty() {
return format!("{}@{}", account_id, domains[0]);
}
account_id.to_string()
}
12 changes: 8 additions & 4 deletions src/common/src/idprovider/himmelblau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@ impl IdProvider for HimmelblauProvider {
// Generate a UserToken, with invalid uuid. We can
// only fetch this from an authenticated token.
let config = self.config.read().await;
// Perform logon name mapping
let mapped_name = config.map_upn_to_name(&account_id);
let gidnumber = match config.get_id_attr_map() {
// If Uuid mapping is enabled, bail out now.
// We can only provide a valid idmapping with
Expand All @@ -625,14 +627,14 @@ impl IdProvider for HimmelblauProvider {
};
let fake_uuid = Uuid::new_v4();
let groups = vec![GroupToken {
name: account_id.clone(),
name: mapped_name.clone(),
spn: account_id.clone(),
uuid: fake_uuid,
gidnumber,
}];
let config = self.config.read().await;
return Ok(UserToken {
name: account_id.clone(),
name: mapped_name.clone(),
spn: account_id.clone(),
uuid: fake_uuid,
gidnumber,
Expand Down Expand Up @@ -1254,6 +1256,8 @@ impl HimmelblauProvider {
return Err(IdpError::BadRequest);
}
};
// Perform logon name mapping
let mapped_name = config.map_upn_to_name(&spn);
let uuid = match value.uuid() {
Ok(uuid) => uuid,
Err(e) => {
Expand Down Expand Up @@ -1304,14 +1308,14 @@ impl HimmelblauProvider {

// Add the fake primary group
groups.push(GroupToken {
name: spn.clone(),
name: mapped_name.clone(),
spn: spn.clone(),
uuid,
gidnumber,
});

Ok(UserToken {
name: spn.clone(),
name: mapped_name.clone(),
spn: spn.clone(),
uuid,
gidnumber,
Expand Down
8 changes: 8 additions & 0 deletions src/config/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@
# logon_script =
# logon_token_scopes =
#
# In some cases, mapping the UPN to the CN may be impractical. The following
# option executes the specifed filename (any executable), which MUST accept
# a single argument which will be EITHER the upn, or the mapped name. The
# executable MUST print a single response to stdout, which will be either the
# mapped name, or the upn, respectively. This executable must map the name
# bi-directionally.
# name_mapping_script =
#
# authority_host = login.microsoftonline.com
#
# The location of the cache database
Expand Down
35 changes: 11 additions & 24 deletions src/glue/src/unix_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,18 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use himmelblau_unix_common::config::HimmelblauConfig;
use himmelblau_unix_common::config::{map_name_to_upn, HimmelblauConfig};
use himmelblau_unix_common::constants::{
DEFAULT_CONN_TIMEOUT, DEFAULT_HELLO_PIN_MIN_LEN, DEFAULT_SOCK_PATH,
};
use himmelblau_unix_common::unix_passwd::parse_etc_passwd;
use std::fs::File;
use std::io::Read;

pub struct KanidmUnixdConfig {
pub domains: Vec<String>,
pub unix_sock_timeout: u64,
pub sock_path: String,
pub cn_name_mapping: bool,
pub hello_pin_min_length: usize,
name_mapping_script: Option<String>,
}

impl KanidmUnixdConfig {
Expand All @@ -39,6 +37,7 @@ impl KanidmUnixdConfig {
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cn_name_mapping: false,
hello_pin_min_length: DEFAULT_HELLO_PIN_MIN_LEN,
name_mapping_script: None,
}
}

Expand All @@ -50,29 +49,17 @@ impl KanidmUnixdConfig {
unix_sock_timeout: config.get_connection_timeout() * 2,
cn_name_mapping: config.get_cn_name_mapping(),
hello_pin_min_length: config.get_hello_pin_min_length(),
name_mapping_script: config.get_name_mapping_script(),
})
}

pub fn map_cn_name(&self, account_id: &str) -> String {
// Make sure this account_id isn't a local user
let mut contents = vec![];
if let Ok(mut file) = File::open("/etc/passwd") {
let _ = file.read_to_end(&mut contents);
}
let local_users = parse_etc_passwd(contents.as_slice()).unwrap_or_default();
if local_users
.into_iter()
.map(|u| u.name.to_string())
.collect::<Vec<String>>()
.contains(&account_id.to_string())
{
return account_id.to_string();
}

if self.cn_name_mapping && !account_id.contains('@') && !self.domains.is_empty() {
return format!("{}@{}", account_id, self.domains[0]);
}
account_id.to_string()
pub fn map_name_to_upn(&self, account_id: &str) -> String {
map_name_to_upn(
account_id,
&self.name_mapping_script,
self.cn_name_mapping,
&self.domains,
)
}
}

Expand Down
9 changes: 3 additions & 6 deletions src/nss/src/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl PasswdHooks for HimmelblauPasswd {
return Response::Unavail;
}
};
let name = cfg.map_cn_name(&name);
let name = cfg.map_name_to_upn(&name);
let req = ClientRequest::NssAccountByName(name.clone());
let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
Ok(dc) => dc,
Expand All @@ -96,11 +96,8 @@ impl PasswdHooks for HimmelblauPasswd {
.call_and_wait(&req, cfg.unix_sock_timeout)
.map(|r| match r {
ClientResponse::NssAccount(opt) => opt
.map(|nu| {
let mut passwd = passwd_from_nssuser(nu);
passwd.name = name;
Response::Success(passwd)
})
.map(passwd_from_nssuser)
.map(Response::Success)
.unwrap_or_else(|| Response::NotFound),
_ => Response::NotFound,
})
Expand Down
6 changes: 3 additions & 3 deletions src/pam/src/pam/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ impl PamHooks for PamKanidm {
Ok(cfg) => cfg,
Err(e) => return e,
};
let account_id = cfg.map_cn_name(&account_id);
let account_id = cfg.map_name_to_upn(&account_id);
let req = ClientRequest::PamAccountAllowed(account_id);
// PamResultCode::PAM_IGNORE

Expand Down Expand Up @@ -371,7 +371,7 @@ impl PamHooks for PamKanidm {
Ok(cfg) => cfg,
Err(e) => return e,
};
let account_id = cfg.map_cn_name(&account_id);
let account_id = cfg.map_name_to_upn(&account_id);

let mut timeout = cfg.unix_sock_timeout;
let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
Expand Down Expand Up @@ -573,7 +573,7 @@ impl PamHooks for PamKanidm {
Ok(cfg) => cfg,
Err(e) => return e,
};
let account_id = cfg.map_cn_name(&account_id);
let account_id = cfg.map_name_to_upn(&account_id);
let req = ClientRequest::PamAccountBeginSession(account_id);

let mut daemon_client = match DaemonClientBlocking::new(cfg.sock_path.as_str()) {
Expand Down
1 change: 1 addition & 0 deletions src/sso/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

0 comments on commit a84b57f

Please sign in to comment.