1
0
mirror of https://github.com/samba-team/samba.git synced 2025-01-08 21:18:16 +03:00

Add the Azure Entra Id Daemon

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
This commit is contained in:
David Mulder 2024-07-10 14:04:28 -06:00
parent f1aa68041e
commit aea926391f
10 changed files with 759 additions and 2 deletions

View File

@ -12,21 +12,36 @@ homepage.workspace = true
version.workspace = true
[dependencies]
libhimmelblau = "0.2.7"
ntstatus_gen.workspace = true
param = { workspace = true }
sock = { workspace = true }
tdb = { workspace = true }
dbg = { workspace = true }
chelps.workspace = true
clap = "4.5.9"
kanidm-hsm-crypto = "0.2.0"
serde_json = "1.0.120"
tokio = { version = "1.38.1", features = [ "rt", "macros" ] }
tokio-util = { version = "0.7.11", features = ["codec"] }
bytes = "1.6.1"
futures = "0.3.30"
serde = "1.0.204"
idmap = { workspace = true }
libc = "0.2.155"
[workspace]
members = [
"chelps", "dbg", "idmap",
"ntstatus_gen",
"param", "tdb",
"param", "sock", "tdb",
]
[workspace.dependencies]
param = { path = "param" }
dbg = { path = "dbg" }
chelps = { path = "chelps" }
sock = { path = "sock" }
ntstatus_gen = { path = "ntstatus_gen" }
tdb = { path = "tdb" }
idmap = { path = "idmap" }

18
himmelblaud/build.rs Normal file
View File

@ -0,0 +1,18 @@
use std::env;
fn main() {
// Re-export the Target OS, so that Himmelblaud has access to this at
// runtime.
if &env::var("CARGO_CFG_TARGET_OS").unwrap() != "none" {
println!(
"cargo:rustc-env=TARGET_OS={}",
&env::var("CARGO_CFG_TARGET_OS").unwrap()
);
} else {
println!(
"cargo:rustc-env=TARGET_OS={}",
&env::var("CARGO_CFG_TARGET_FAMILY").unwrap()
);
}
println!("cargo:rerun-if-changed-env=TARGET");
}

View File

@ -0,0 +1,12 @@
[package]
name = "sock"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[dependencies]
libc = "0.2.155"
libnss = "0.8.0"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"

View File

@ -0,0 +1,57 @@
/*
Unix SMB/CIFS implementation.
Unix socket communication for the Himmelblau daemon
Copyright (C) David Mulder 2024
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
mod proto;
pub use proto::*;
use serde_json::{from_slice as json_from_slice, to_vec as json_to_vec};
use std::error::Error;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
pub struct ClientStream {
stream: UnixStream,
}
impl ClientStream {
pub fn new(path: &str) -> Result<Self, Box<dyn Error>> {
Ok(ClientStream {
stream: UnixStream::connect(path)?,
})
}
pub fn send(
&mut self,
req: &Request,
timeout: u64,
) -> Result<Response, Box<dyn Error>> {
let timeout = Duration::from_secs(timeout);
self.stream.set_read_timeout(Some(timeout))?;
self.stream.set_write_timeout(Some(timeout))?;
let req_bytes = json_to_vec(req)?;
self.stream.write_all(&req_bytes)?;
let mut buf = Vec::new();
self.stream.read_to_end(&mut buf)?;
let resp: Response = json_from_slice(&buf)?;
Ok(resp)
}
}

View File

@ -0,0 +1,115 @@
/*
Unix SMB/CIFS implementation.
Unix socket communication for the Himmelblau daemon
Copyright (C) David Mulder 2024
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
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 libc::uid_t;
use libnss::group::Group as NssGroup;
use libnss::passwd::Passwd as NssPasswd;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Passwd {
pub name: String,
pub passwd: String,
pub uid: u32,
pub gid: u32,
pub gecos: String,
pub dir: String,
pub shell: String,
}
impl From<Passwd> for NssPasswd {
fn from(val: Passwd) -> Self {
NssPasswd {
name: val.name,
passwd: val.passwd,
uid: val.uid,
gid: val.gid,
gecos: val.gecos,
dir: val.dir,
shell: val.shell,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Group {
pub name: String,
pub passwd: String,
pub gid: u32,
pub members: Vec<String>,
}
impl From<Group> for NssGroup {
fn from(val: Group) -> Self {
NssGroup {
name: val.name,
passwd: val.passwd,
gid: val.gid,
members: val.members,
}
}
}
#[derive(Serialize, Deserialize)]
pub enum PamAuthRequest {
Password { cred: String },
MFACode { cred: String },
MFAPoll { poll_attempt: u32 },
SetupPin { pin: String },
Pin { pin: String },
}
#[derive(Serialize, Deserialize)]
pub enum Request {
NssAccounts,
NssAccountByUid(uid_t),
NssAccountByName(String),
NssGroups,
NssGroupByGid(uid_t),
NssGroupByName(String),
PamAuthenticateInit(String),
PamAuthenticateStep(PamAuthRequest),
PamAccountAllowed(String),
PamAccountBeginSession(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub enum PamAuthResponse {
Unknown,
Success,
Denied,
Password,
MFACode { msg: String },
MFAPoll { msg: String, polling_interval: u32 },
SetupPin { msg: String },
Pin,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Response {
NssAccounts(Vec<Passwd>),
NssAccount(Option<Passwd>),
NssGroups(Vec<Group>),
NssGroup(Option<Group>),
PamStatus(Option<bool>),
PamAuthStepResponse(PamAuthResponse),
Success,
Error,
}

View File

@ -0,0 +1 @@
pub const DEFAULT_ODC_PROVIDER: &str = "odc.officeapps.live.com";

View File

@ -0,0 +1,153 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon
Copyright (C) David Mulder 2024
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
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::cache::{GroupCache, PrivateCache, UserCache};
use bytes::{BufMut, BytesMut};
use dbg::{DBG_DEBUG, DBG_ERR};
use futures::{SinkExt, StreamExt};
use himmelblau::graph::Graph;
use himmelblau::BrokerClientApplication;
use idmap::Idmap;
use kanidm_hsm_crypto::{BoxedDynTpm, MachineKey};
use param::LoadParm;
use sock::{Request, Response};
use std::error::Error;
use std::io;
use std::io::{Error as IoError, ErrorKind};
use std::sync::Arc;
use tokio::net::UnixStream;
use tokio::sync::Mutex;
use tokio_util::codec::{Decoder, Encoder, Framed};
pub(crate) struct Resolver {
realm: String,
tenant_id: String,
lp: LoadParm,
idmap: Idmap,
graph: Graph,
pcache: PrivateCache,
user_cache: UserCache,
group_cache: GroupCache,
hsm: Mutex<BoxedDynTpm>,
machine_key: MachineKey,
client: Arc<Mutex<BrokerClientApplication>>,
}
impl Resolver {
pub(crate) fn new(
realm: &str,
tenant_id: &str,
lp: LoadParm,
idmap: Idmap,
graph: Graph,
pcache: PrivateCache,
user_cache: UserCache,
group_cache: GroupCache,
hsm: BoxedDynTpm,
machine_key: MachineKey,
client: BrokerClientApplication,
) -> Self {
Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
graph,
pcache,
user_cache,
group_cache,
hsm: Mutex::new(hsm),
machine_key,
client: Arc::new(Mutex::new(client)),
}
}
}
struct ClientCodec;
impl Decoder for ClientCodec {
type Error = io::Error;
type Item = Request;
fn decode(
&mut self,
src: &mut BytesMut,
) -> Result<Option<Self::Item>, Self::Error> {
match serde_json::from_slice::<Request>(src) {
Ok(msg) => {
src.clear();
Ok(Some(msg))
}
_ => Ok(None),
}
}
}
impl Encoder<Response> for ClientCodec {
type Error = io::Error;
fn encode(
&mut self,
msg: Response,
dst: &mut BytesMut,
) -> Result<(), Self::Error> {
DBG_DEBUG!("Attempting to send response -> {:?} ...", msg);
let data = serde_json::to_vec(&msg).map_err(|e| {
DBG_ERR!("socket encoding error -> {:?}", e);
io::Error::new(ErrorKind::Other, "JSON encode error")
})?;
dst.put(data.as_slice());
Ok(())
}
}
impl ClientCodec {
fn new() -> Self {
ClientCodec
}
}
pub(crate) async fn handle_client(
stream: UnixStream,
resolver: Arc<Mutex<Resolver>>,
) -> Result<(), Box<dyn Error>> {
DBG_DEBUG!("Accepted connection");
let Ok(_ucred) = stream.peer_cred() else {
return Err(Box::new(IoError::new(
ErrorKind::Other,
"Unable to verify peer credentials.",
)));
};
let mut reqs = Framed::new(stream, ClientCodec::new());
while let Some(Ok(req)) = reqs.next().await {
let resp = match req {
_ => todo!(),
};
reqs.send(resp).await?;
reqs.flush().await?;
DBG_DEBUG!("flushed response!");
}
DBG_DEBUG!("Disconnecting client ...");
Ok(())
}

View File

@ -1 +1,382 @@
fn main() {}
/*
Unix SMB/CIFS implementation.
Himmelblau daemon
Copyright (C) David Mulder 2024
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
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 clap::{Arg, ArgAction, Command};
use dbg::*;
use himmelblau::graph::Graph;
use himmelblau::BrokerClientApplication;
use idmap::Idmap;
use kanidm_hsm_crypto::soft::SoftTpm;
use kanidm_hsm_crypto::{BoxedDynTpm, Tpm};
use param::LoadParm;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::net::UnixListener;
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::Mutex;
mod constants;
use constants::DEFAULT_ODC_PROVIDER;
mod cache;
mod himmelblaud;
use cache::{GroupCache, PrivateCache, UserCache};
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
let clap_args = Command::new("himmelblaud")
.version(env!("CARGO_PKG_VERSION"))
.about("Samba Himmelblau Authentication Daemon")
.arg(
Arg::new("debuglevel")
.help("Set debug level")
.short('d')
.long("debuglevel")
.value_parser(
clap::value_parser!(u16).range(0..(MAX_DEBUG_LEVEL as i64)),
)
.action(ArgAction::Set),
)
.arg(
Arg::new("debug-stdout")
.help("Send debug output to standard output")
.long("debug-stdout")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("configfile")
.help("Use alternative configuration file")
.short('s')
.long("configfile")
.action(ArgAction::Set),
)
.get_matches();
let stop_now = Arc::new(AtomicBool::new(false));
let terminate_now = Arc::clone(&stop_now);
let quit_now = Arc::clone(&stop_now);
let interrupt_now = Arc::clone(&stop_now);
async {
// Set the command line debug level
if let Some(debuglevel) = clap_args.get_one::<u16>("debuglevel") {
debuglevel_set!(*debuglevel);
}
// Initialize the LoadParm from the command line specified config file
let lp = match LoadParm::new(
clap_args
.get_one::<String>("configfile")
.map(|x| x.as_str()),
) {
Ok(lp) => lp,
Err(e) => {
eprintln!("Failed loading smb.conf: {:?}", e);
return ExitCode::FAILURE;
}
};
// Check that the realm is configured. This the bare minimum for
// himmelblaud, since a device join can happen at authentication time,
// but we need to know the permitted enrollment domain.
let realm = match lp.realm() {
Ok(Some(realm)) => realm,
_ => {
eprintln!(
"The realm MUST be set in the \
smb.conf to start himmelblaud"
);
return ExitCode::FAILURE;
}
};
// Setup logging, either to the configured logfile, or to stdout, depending
// on what is specified on the command line.
match lp.logfile() {
Ok(Some(logfile)) => debug_set_logfile(&logfile),
_ => {
eprintln!("Failed to determine logfile name");
return ExitCode::FAILURE;
}
}
match clap_args.get_flag("debug-stdout") {
true => setup_logging(env!("CARGO_PKG_NAME"), DEBUG_STDOUT),
false => setup_logging(env!("CARGO_PKG_NAME"), DEBUG_FILE),
}
// Determine the unix socket path
let sock_dir_str = match lp.winbindd_socket_directory() {
Ok(Some(sock_dir)) => sock_dir,
_ => return ExitCode::FAILURE,
};
let sock_dir = Path::new(&sock_dir_str);
let mut sock_path = PathBuf::from(sock_dir);
sock_path.push("hb_pipe");
let sock_path = match sock_path.to_str() {
Some(sock_path) => sock_path,
None => return ExitCode::FAILURE,
};
// Initialize the Himmelblau cache
let private_cache_path = match lp.private_path("himmelblau.tdb") {
Ok(Some(private_cache_path)) => private_cache_path,
_ => {
DBG_ERR!("Failed to determine private cache path");
return ExitCode::FAILURE;
}
};
let mut pcache = match PrivateCache::new(&private_cache_path) {
Ok(cache) => cache,
Err(e) => {
DBG_ERR!(
"Failed to open the himmelblau private cache: {:?}",
e
);
return ExitCode::FAILURE;
}
};
let cache_dir = match lp.cache_directory() {
Ok(Some(cache_dir)) => cache_dir,
_ => {
DBG_ERR!("Failed to determine cache directory");
return ExitCode::FAILURE;
}
};
let user_cache_path = Path::new(&cache_dir)
.join("himmelblau_users.tdb")
.display()
.to_string();
let user_cache = match UserCache::new(&user_cache_path) {
Ok(cache) => cache,
Err(e) => {
DBG_ERR!("Failed to open the himmelblau user cache: {:?}", e);
return ExitCode::FAILURE;
}
};
let group_cache_path = Path::new(&cache_dir)
.join("himmelblau_groups.tdb")
.display()
.to_string();
let group_cache = match GroupCache::new(&group_cache_path) {
Ok(cache) => cache,
Err(e) => {
DBG_ERR!("Failed to open the himmelblau group cache: {:?}", e);
return ExitCode::FAILURE;
}
};
// Check for and create the hsm pin if required.
let auth_value = match pcache.hsm_pin_fetch_or_create() {
Ok(auth_value) => auth_value,
Err(e) => {
DBG_ERR!("{:?}", e);
return ExitCode::FAILURE;
}
};
// Setup the HSM and its machine key
let mut hsm: BoxedDynTpm = BoxedDynTpm::new(SoftTpm::new());
let loadable_machine_key = match pcache
.loadable_machine_key_fetch_or_create(&mut hsm, &auth_value)
{
Ok(lmk) => lmk,
Err(e) => {
DBG_ERR!("{:?}", e);
return ExitCode::FAILURE;
}
};
let res = hsm.machine_key_load(&auth_value, &loadable_machine_key);
let machine_key = match res {
Ok(machine_key) => machine_key,
Err(e) => {
DBG_ERR!("Unable to load machine root key: {:?}", e);
DBG_INFO!("This can occur if you have changed your HSM pin.");
DBG_INFO!(
"To proceed, run `tdbtool erase {}`",
private_cache_path
);
DBG_INFO!("The host will forget domain enrollments.");
return ExitCode::FAILURE;
}
};
// Get the transport key for a joined domain
let loadable_transport_key =
pcache.loadable_transport_key_fetch(&realm);
// Get the certificate key for a joined domain
let loadable_cert_key = pcache.loadable_cert_key_fetch(&realm);
// Contact the odc provider to get the authority host and tenant id
let graph = match Graph::new(DEFAULT_ODC_PROVIDER, &realm).await {
Ok(graph) => graph,
Err(e) => {
DBG_ERR!("Failed initializing the graph: {:?}", e);
return ExitCode::FAILURE;
}
};
let authority_host = graph.authority_host();
let tenant_id = graph.tenant_id();
let authority = format!("https://{}/{}", authority_host, tenant_id);
let client = match BrokerClientApplication::new(
Some(&authority),
loadable_transport_key,
loadable_cert_key,
) {
Ok(client) => client,
Err(e) => {
DBG_ERR!("Failed initializing the broker: {:?}", e);
return ExitCode::FAILURE;
}
};
let mut idmap = match Idmap::new() {
Ok(idmap) => idmap,
Err(e) => {
DBG_ERR!("Failed initializing the idmapper: {:?}", e);
return ExitCode::FAILURE;
}
};
// Configure the idmap range
let (low, high) = match lp.idmap_range(&realm) {
Ok(res) => res,
Err(e) => {
DBG_ERR!("Failed fetching idmap range: {:?}", e);
return ExitCode::FAILURE;
}
};
if let Err(e) = idmap.add_gen_domain(&realm, &tenant_id, (low, high)) {
DBG_ERR!("Failed adding the domain idmap range: {:?}", e);
return ExitCode::FAILURE;
}
let resolver = Arc::new(Mutex::new(himmelblaud::Resolver::new(
&realm,
&tenant_id,
lp,
idmap,
graph,
pcache,
user_cache,
group_cache,
hsm,
machine_key,
client,
)));
// Listen for incomming requests from PAM and NSS
let listener = match UnixListener::bind(sock_path) {
Ok(listener) => listener,
Err(e) => {
DBG_ERR!("Failed setting up the socket listener: {:?}", e);
return ExitCode::FAILURE;
}
};
let server = tokio::spawn(async move {
while !stop_now.load(Ordering::Relaxed) {
let resolver_ref = resolver.clone();
match listener.accept().await {
Ok((socket, _addr)) => {
tokio::spawn(async move {
if let Err(e) = himmelblaud::handle_client(
socket,
resolver_ref.clone(),
)
.await
{
DBG_ERR!(
"handle_client error occurred: {:?}",
e
);
}
});
}
Err(e) => {
DBG_ERR!("Error while handling connection: {:?}", e);
}
}
}
});
let terminate_task = tokio::spawn(async move {
match signal(SignalKind::terminate()) {
Ok(mut stream) => {
stream.recv().await;
terminate_now.store(true, Ordering::Relaxed);
}
Err(e) => {
DBG_ERR!("Failed registering terminate signal: {}", e);
}
};
});
let quit_task = tokio::spawn(async move {
match signal(SignalKind::quit()) {
Ok(mut stream) => {
stream.recv().await;
quit_now.store(true, Ordering::Relaxed);
}
Err(e) => {
DBG_ERR!("Failed registering quit signal: {}", e);
}
};
});
let interrupt_task = tokio::spawn(async move {
match signal(SignalKind::interrupt()) {
Ok(mut stream) => {
stream.recv().await;
interrupt_now.store(true, Ordering::Relaxed);
}
Err(e) => {
DBG_ERR!("Failed registering interrupt signal: {}", e);
}
};
});
DBG_INFO!("Server started ...");
tokio::select! {
_ = server => {
DBG_DEBUG!("Main listener task is terminating");
},
_ = terminate_task => {
DBG_DEBUG!("Received signal to terminate");
},
_ = quit_task => {
DBG_DEBUG!("Received signal to quit");
},
_ = interrupt_task => {
DBG_DEBUG!("Received signal to interrupt");
}
}
ExitCode::SUCCESS
}
.await
}

View File

@ -0,0 +1,4 @@
#!/usr/bin/env python
bld.SAMBA_RUST_BINARY('himmelblaud',
source='src/main.rs param/src/lib.rs chelps/src/lib.rs dbg/src/lib.rs ntstatus_gen/src/lib.rs sock/src/lib.rs tdb/src/lib.rs version/src/lib.rs')

View File

@ -155,6 +155,7 @@ bld.RECURSE('dfs_server')
bld.RECURSE('file_server')
bld.RECURSE('lib/krb5_wrap')
bld.RECURSE('packaging')
bld.RECURSE('himmelblaud')
bld.RECURSE('testsuite/headers')