1
0
mirror of https://github.com/samba-team/samba.git synced 2024-12-31 17:18:04 +03:00

Compare commits

...

65 Commits

Author SHA1 Message Date
David Mulder
826b75bf03 Fix pam failure to register Pin following mfa poll
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>

Autobuild-User(master): David Mulder <dmulder@samba.org>
Autobuild-Date(master): Wed Oct 23 15:39:09 UTC 2024 on atb-devel-224
2024-10-23 15:39:09 +00:00
David Mulder
f5048e3d98 autobuild: Configure samba-o3 for himmelblau testing
The 'samba' build excludes the 'none' tests,
which is where the Rust tests are located.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
a18c6ff20b Fix usage test broken by rust vendor sources
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
24710a5f4e autobuild: Only enable rust build if cargo exists
We need to do this to prevent CI systems which
are missing cargo (Debian) from attempting to
configure with Rust.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
549f67a9e6 Add configure checks for glibc and openssl versions
Add configure checks to determine if rust or
specifically himmelblau sources can be compiled.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
221447d5ba glibc needs to be at least vers 2.32 for rust
The libc crate won't build on versions older than
2.32.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
882a761a50 autobuild: Only configure himmelblau if openssl >= 3
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
986e398887 Add pyglue for Rust for disabling tests
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
910a331f3e Disable the rust build by default
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
0998686355 Fix Rocky8 build for utf8proc-devel
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
8a75cb0e20 Fix pam echo not displayed via ssh
Necessary because of OpenSSH bug
https://bugzilla.mindrot.org/show_bug.cgi?id=2876 -
PAM_TEXT_INFO and PAM_ERROR_MSG conversation not
honoured during PAM authentication

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
a8491e83b4 Add the user's primary group to the cache
We create a fake primary group which simply
matches the user's upn. This is because Entra ID
does not have primary groups, but we can fake it
with a primary group which is a member of all the
users groups.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
d100aaf4bf Fetch the target join os via std::env::consts
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
0bcc209d94 Properly handle read/write from the client socket
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
6907508cdb Ensure clients can write to the himmelblau pipe
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
cb23d6a15e Remove the existing socket if present
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
066576e7c8 Use the s4 param functions to access idmap values
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
6a60174c04 Always print a newline at the end of debug msgs
Otherwise the message doesn't flush to the log
and could be lost when the program terminates.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
bb55fa100e Add talloc stackframe handling
This appeases errors from libsmbconf that no
talloc stackframe was created.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
8f8943547b Fix display of function names in debug
Rust adds some odd `{{closure}}` bits to the
function name that need to be removed, otherwise
the debug is unreadable.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
a72146c4ef Add warnings for missing directories at runtime
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
c33fb4bfd5 Only set the debug logfile if not stdout
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
ff0cf5eda1 Add tests for rust himmelblaud build
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
89db2e536c Always normalize cache inputs to lowercase
This prevents mixed case issues when storing/
retrieving data from the cache.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
9bd37a450d Add tests for rust dbg crate
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
8298cf4376 Improve cargo test output
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
7140c506ee Add USING_SYSTEM_TDB test for rust config
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
518854e29b Add tests for rust chelps crate
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
4ac7e56bca Debian cargo is far to old for building
Debian ships a very old version of cargo, and
none of our rust code can build with it.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
43971bbada Vendor the rust sources for CI tests
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
5a8b8a7799 Rust WAF detect dependant files from crates
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:34 +00:00
David Mulder
4a4304ecf7 Modify rust build to share target dir w/ cargo test
The build needs to share the target directory
with the cargo test command to prevent duplicate
dependency downloads.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
33bdebb7c8 Enable rust cargo test in Samba make test
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
7273cc7d89 Adding Cargo.lock file for Rust build
This ensures that all builds of this particular
version of Samba will use the same Rust
dependency versions.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
57353b8650 A Rust 'crate' is not a misspelling of 'create'
Also fix a misspelling in himmelblaud main.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
2824ed2605 utf8proc-devel is missing from CentOS 8 Stream
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
58fbba3897 Introduce build option to enable Himmelblau
The build for Himmelblau will be disabled by
default.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
9ffac29435 Add clang and openssl deps
Ensure CI images are generated with the necessary
clang and openssl packages for building.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
35b34e8e42 Add build config for proper TDB build linkage
The tdb build needs to know whether Samba is
building with TDB bundled or not, otherwise
linking will fail.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
dc445490cc Update libhimmelblau
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
e3715ba548 Pam prompt for Pin if hello enrolled and enabled
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
948d0fcfe1 Isolate hsm auth value from the cache
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
abcf7644a9 Reorganize rust file tree
Place all rust code under samba/rust, similar to
how we organize python code in the samba tree.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
3f6254a670 Add pam_open_session stub to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
5350b31a78 Add pam_acct_mgmt to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
426c43963a Add nss getgrgid to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
3c55599e99 Add nss getgrnam to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
a5e330ee90 Add nss getgrent to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
71b96f78df Add nss getpwuid to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
94e4d005b5 Add nss getpwnam to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
82d7208a85 Add nss getpwent to the himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
e243b7c95f Add pam_auth to himmelblau daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
f6a22875d1 Add PAM module for himmelblaud
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
e4c28a2d57 Add Samba versioning
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
1e2abfa313 Add NSS module for himmelblaud
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
aea926391f Add the Azure Entra Id Daemon
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
f1aa68041e Add a daemon caching layer that wraps tdb
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
92dca744e1 Add by-upn idmapping for Himmelblaud
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
f7edbf70d0 Add rust tdb bindings
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
f0cbe4d5a2 Add lp Rust bindings
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
61a5dc11a7 Build the Rust ntstatus generated code
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
f37e311af1 Add rust bindings to Samba utils debug
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
03c0c89809 Add Rust formatting for 80 char lines
This option helps force `cargo fmt` to follow
Samba's coding conventions. The 80 char max
is only lightly enforced here.

Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
8ac241017f Add ntstatus_gen for Rust
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
David Mulder
94be411a94 Add simple WAF commands for building Rust bininaries
Signed-off-by: David Mulder <dmulder@samba.org>
Reviewed-by: Alexander Bokovoy <ab@samba.org>
2024-10-23 14:21:33 +00:00
121 changed files with 14438 additions and 7 deletions

View File

@ -10,6 +10,7 @@ bre
bu
childres
clen
crate
creat
connectin
daa

View File

@ -47,7 +47,7 @@ variables:
# Set this to the contents of bootstrap/sha1sum.txt
# which is generated by bootstrap/template.py --render
#
SAMBA_CI_CONTAINER_TAG: d1ce7e10953d16253a34b8e58077fd32c1dbd59c
SAMBA_CI_CONTAINER_TAG: 936722ecb26bedf6ea0acd9228963ce45ed419d4
#
# We use the ubuntu2204 image as default as
# it matches what we have on atb-devel-224

View File

@ -64,6 +64,7 @@ COMMON = [
'tar',
'tree',
'wget',
'cargo',
]
@ -83,6 +84,9 @@ PKGS = [
('libcap-dev', 'libcap-devel'),
('libacl1-dev', 'libacl-devel'),
('libattr1-dev', 'libattr-devel'),
('libutf8proc-dev', 'utf8proc-devel'),
('libssl-dev', 'openssl-devel'),
('libclang-dev', 'clang-devel'),
# libNAME1-dev, NAME2-devel
('libpopt-dev', 'popt-devel'),
@ -244,6 +248,11 @@ set -xueo pipefail
yum update -y
yum install -y dnf-plugins-core
yum install -y epel-release
yum install -y centos-release-ceph-pacific
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-Ceph-*
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Ceph-*
sed -i 's/$contentdir/centos/g' /etc/yum.repos.d/CentOS-Ceph-*
yum -v repolist all
yum config-manager --set-enabled powertools -y
@ -252,6 +261,7 @@ yum update -y
yum install -y \
--setopt=install_weak_deps=False \
--setopt=centos-ceph-pacific.module_hotfixes=true \
{pkgs}
yum clean all
@ -443,6 +453,7 @@ DEB_DISTS = {
'replace': {
'language-pack-en': '', # included in locales
'shfmt': '',
'cargo': '', # included cargo is broken
}
},
'debian11-32bit': {
@ -451,6 +462,7 @@ DEB_DISTS = {
'replace': {
'language-pack-en': '', # included in locales
'shfmt': '',
'cargo': '', # included cargo is broken
}
},
'debian12': {
@ -459,6 +471,7 @@ DEB_DISTS = {
'replace': {
'language-pack-en': '', # included in locales
'libtracker-sparql-2.0-dev': '', # only tracker 3.x is available
'cargo': '', # included cargo is broken
}
},
'debian12-32bit': {
@ -467,6 +480,7 @@ DEB_DISTS = {
'replace': {
'language-pack-en': '', # included in locales
'libtracker-sparql-2.0-dev': '', # only tracker 3.x is available
'cargo': '', # included cargo is broken
}
},
'ubuntu1804': {

View File

@ -27,8 +27,10 @@ dnf install -y \
bind-utils \
binutils \
bison \
cargo \
ccache \
chrpath \
clang-devel \
crypto-policies-scripts \
cups-devel \
dbus-devel \
@ -78,6 +80,7 @@ dnf install -y \
mingw64-gcc \
ncurses-devel \
openldap-devel \
openssl-devel \
pam-devel \
patch \
perl \
@ -115,6 +118,7 @@ dnf install -y \
tar \
tracker-devel \
tree \
utf8proc-devel \
wget \
which \
xfsprogs-devel \

View File

@ -8,8 +8,10 @@ packages:
- bind-utils
- binutils
- bison
- cargo
- ccache
- chrpath
- clang-devel
- crypto-policies-scripts
- cups-devel
- dbus-devel
@ -59,6 +61,7 @@ packages:
- mingw64-gcc
- ncurses-devel
- openldap-devel
- openssl-devel
- pam-devel
- patch
- perl
@ -96,6 +99,7 @@ packages:
- tar
- tracker-devel
- tree
- utf8proc-devel
- wget
- which
- xfsprogs-devel

View File

@ -50,6 +50,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -68,12 +69,14 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libtracker-sparql-2.0-dev \
libunwind-dev \
liburing-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -39,6 +39,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -57,12 +58,14 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libtracker-sparql-2.0-dev
- libunwind-dev
- liburing-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -50,6 +50,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -68,12 +69,14 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libtracker-sparql-2.0-dev \
libunwind-dev \
liburing-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -39,6 +39,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -57,12 +58,14 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libtracker-sparql-2.0-dev
- libunwind-dev
- liburing-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -50,6 +50,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -68,11 +69,13 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libunwind-dev \
liburing-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -39,6 +39,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -57,11 +58,13 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libunwind-dev
- liburing-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -50,6 +50,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -68,11 +69,13 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libunwind-dev \
liburing-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -39,6 +39,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -57,11 +58,13 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libunwind-dev
- liburing-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -20,8 +20,10 @@ dnf install -y \
bind-utils \
binutils \
bison \
cargo \
ccache \
chrpath \
clang-devel \
codespell \
crypto-policies-scripts \
cups-devel \
@ -75,6 +77,7 @@ dnf install -y \
mold \
ncurses-devel \
openldap-devel \
openssl-devel \
pam-devel \
patch \
perl \
@ -114,6 +117,7 @@ dnf install -y \
tar \
tracker-devel \
tree \
utf8proc-devel \
wget \
which \
xfsprogs-devel \

View File

@ -9,8 +9,10 @@ packages:
- bind-utils
- binutils
- bison
- cargo
- ccache
- chrpath
- clang-devel
- codespell
- crypto-policies-scripts
- cups-devel
@ -64,6 +66,7 @@ packages:
- mold
- ncurses-devel
- openldap-devel
- openssl-devel
- pam-devel
- patch
- perl
@ -103,6 +106,7 @@ packages:
- tar
- tracker-devel
- tree
- utf8proc-devel
- wget
- which
- xfsprogs-devel

View File

@ -20,8 +20,10 @@ zypper --non-interactive install \
bind-utils \
binutils \
bison \
cargo \
ccache \
chrpath \
clang-devel \
codespell \
crypto-policies-scripts \
cups-devel \
@ -72,6 +74,7 @@ zypper --non-interactive install \
mingw64-gcc \
ncurses-devel \
openldap2-devel \
openssl-devel \
pam-devel \
patch \
perl \
@ -106,6 +109,7 @@ zypper --non-interactive install \
tar \
tracker-devel \
tree \
utf8proc-devel \
wget \
which \
xfsprogs-devel \

View File

@ -8,8 +8,10 @@ packages:
- bind-utils
- binutils
- bison
- cargo
- ccache
- chrpath
- clang-devel
- codespell
- crypto-policies-scripts
- cups-devel
@ -60,6 +62,7 @@ packages:
- mingw64-gcc
- ncurses-devel
- openldap2-devel
- openssl-devel
- pam-devel
- patch
- perl
@ -94,6 +97,7 @@ packages:
- tar
- tracker-devel
- tree
- utf8proc-devel
- wget
- which
- xfsprogs-devel

View File

@ -10,6 +10,11 @@ set -xueo pipefail
yum update -y
yum install -y dnf-plugins-core
yum install -y epel-release
yum install -y centos-release-ceph-pacific
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-Ceph-*
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Ceph-*
sed -i 's/$contentdir/centos/g' /etc/yum.repos.d/CentOS-Ceph-*
yum -v repolist all
yum config-manager --set-enabled powertools -y
@ -18,6 +23,7 @@ yum update -y
yum install -y \
--setopt=install_weak_deps=False \
--setopt=centos-ceph-pacific.module_hotfixes=true \
"@Development Tools" \
acl \
attr \
@ -26,8 +32,10 @@ yum install -y \
bind-utils \
binutils \
bison \
cargo \
ccache \
chrpath \
clang-devel \
crypto-policies-scripts \
cups-devel \
curl \
@ -77,6 +85,7 @@ yum install -y \
mingw64-gcc \
ncurses-devel \
openldap-devel \
openssl-devel \
pam-devel \
patch \
perl \
@ -115,6 +124,7 @@ yum install -y \
tar \
tracker-devel \
tree \
utf8proc-devel \
wget \
which \
xfsprogs-devel \

View File

@ -8,8 +8,10 @@ packages:
- bind-utils
- binutils
- bison
- cargo
- ccache
- chrpath
- clang-devel
- crypto-policies-scripts
- cups-devel
- curl
@ -59,6 +61,7 @@ packages:
- mingw64-gcc
- ncurses-devel
- openldap-devel
- openssl-devel
- pam-devel
- patch
- perl
@ -97,6 +100,7 @@ packages:
- tar
- tracker-devel
- tree
- utf8proc-devel
- wget
- which
- xfsprogs-devel

View File

@ -19,6 +19,7 @@ apt-get -y install \
binutils \
bison \
build-essential \
cargo \
ccache \
chrpath \
codespell \
@ -51,6 +52,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -69,11 +71,13 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libtracker-sparql-2.0-dev \
libunwind-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -8,6 +8,7 @@ packages:
- binutils
- bison
- build-essential
- cargo
- ccache
- chrpath
- codespell
@ -40,6 +41,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -58,11 +60,13 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libtracker-sparql-2.0-dev
- libunwind-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -19,6 +19,7 @@ apt-get -y install \
binutils \
bison \
build-essential \
cargo \
ccache \
chrpath \
codespell \
@ -51,6 +52,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -69,11 +71,13 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libtracker-sparql-2.0-dev \
libunwind-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -8,6 +8,7 @@ packages:
- binutils
- bison
- build-essential
- cargo
- ccache
- chrpath
- codespell
@ -40,6 +41,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -58,11 +60,13 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libtracker-sparql-2.0-dev
- libunwind-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -19,6 +19,7 @@ apt-get -y install \
binutils \
bison \
build-essential \
cargo \
ccache \
chrpath \
codespell \
@ -51,6 +52,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -69,11 +71,13 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libtracker-sparql-2.0-dev \
libunwind-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -8,6 +8,7 @@ packages:
- binutils
- bison
- build-essential
- cargo
- ccache
- chrpath
- codespell
@ -40,6 +41,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -58,11 +60,13 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libtracker-sparql-2.0-dev
- libunwind-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -19,6 +19,7 @@ apt-get -y install \
binutils \
bison \
build-essential \
cargo \
ccache \
chrpath \
codespell \
@ -51,6 +52,7 @@ apt-get -y install \
libbsd-dev \
libcap-dev \
libcephfs-dev \
libclang-dev \
libcups2-dev \
libdbus-1-dev \
libglib2.0-dev \
@ -69,11 +71,13 @@ apt-get -y install \
libpcap-dev \
libpopt-dev \
libreadline-dev \
libssl-dev \
libsystemd-dev \
libtasn1-bin \
libtasn1-dev \
libunwind-dev \
liburing-dev \
libutf8proc-dev \
lmdb-utils \
locales \
lsb-release \

View File

@ -8,6 +8,7 @@ packages:
- binutils
- bison
- build-essential
- cargo
- ccache
- chrpath
- codespell
@ -40,6 +41,7 @@ packages:
- libbsd-dev
- libcap-dev
- libcephfs-dev
- libclang-dev
- libcups2-dev
- libdbus-1-dev
- libglib2.0-dev
@ -58,11 +60,13 @@ packages:
- libpcap-dev
- libpopt-dev
- libreadline-dev
- libssl-dev
- libsystemd-dev
- libtasn1-bin
- libtasn1-dev
- libunwind-dev
- liburing-dev
- libutf8proc-dev
- lmdb-utils
- locales
- lsb-release

View File

@ -1 +1 @@
d1ce7e10953d16253a34b8e58077fd32c1dbd59c
936722ecb26bedf6ea0acd9228963ce45ed419d4

View File

@ -0,0 +1,89 @@
from waflib.Configure import conf
from waflib import Build
import os
@conf
def SAMBA_CHECK_RUST(conf):
conf.find_program('cargo', var='CARGO',
mandatory=conf.env.enable_rust)
def vendor_sources(bld, enabled=True):
# force-disable when we can't build rust modules, so
# every single call doesn't need to pass this in.
if not bld.env.enable_rust:
enabled = False
# Save time, no need to build rust when fuzzing
if bld.env.enable_fuzzing:
enabled = False
# Determine the vendor directory
vendor = bld.path.find_or_declare('./vendor')
# WAF dependencies can only be explicit files, not directories, so we touch
# a file to indicate vendoring has been completed.
vendor_exists = '%s.exists' % vendor
# Locate the source manifest file
source_manifest = bld.path.find_or_declare('../../../rust/Cargo.toml')
rule = ['${CARGO}', 'vendor',
'--manifest-path=${SRC[0].abspath(env)}',
'%s' % vendor,
'&& touch %s' % vendor_exists]
bld.SAMBA_GENERATOR('vendor.exists',
' '.join(rule),
source=source_manifest,
target=vendor_exists,
group='final',
enabled=enabled)
Build.BuildContext.vendor_sources = vendor_sources
def find_sources(source_dir, dep_crate):
sources = []
for root, dirs, files in os.walk(os.path.join(source_dir, dep_crate)):
for file in files:
if os.path.splitext(file)[-1] in ['.rs', '.c', '.h']:
sources.append(os.path.join(root, file))
return sources
def SAMBA_RUST(bld, rust_subdir, target_name, dep_crates=[], enabled=True):
# force-disable when we can't build rust modules, so
# every single call doesn't need to pass this in.
if not bld.env.enable_rust:
enabled = False
# Save time, no need to build rust when fuzzing
if bld.env.enable_fuzzing:
enabled = False
release_flag = ''
if bld.env.debug or bld.env.developer:
target = os.path.join('debug', target_name)
else:
release_flag = '--release'
target = os.path.join('release', target_name)
target = bld.path.find_or_declare(target)
# The Rust target directory is one directory above the located target
target_dir = os.path.join(os.path.dirname('%s' % target), '../')
# Try to determine the source directory
source_dir = os.path.abspath(os.path.join(target_dir, '../../../rust'))
if not os.path.exists(source_dir):
raise Exception('Failed to determine rust source directory')
# Now determine the sources of each local crate
sources = find_sources(source_dir, rust_subdir)
for dep_crate in dep_crates:
sources.extend(find_sources(source_dir, dep_crate))
sources = [os.path.relpath(p, source_dir) for p in sources]
rule = ['${CARGO}', 'build',
'--manifest-path=${SRC[0].abspath(env)}',
'--target-dir=%s' % target_dir,
release_flag]
bld.SAMBA_GENERATOR(target_name,
' '.join(rule),
source='%s/Cargo.toml vendor.exists %s' % \
(rust_subdir, ' '.join(sources)),
target=target,
group='final',
enabled=enabled)
Build.BuildContext.SAMBA_RUST_LIBRARY = SAMBA_RUST
Build.BuildContext.SAMBA_RUST_BINARY = SAMBA_RUST

View File

@ -19,6 +19,7 @@ from samba_autoproto import *
from samba_python import *
from samba_perl import *
from samba_deps import *
from samba_rust import *
from samba_bundled import *
from samba_third_party import *
import samba_cross

View File

@ -8,6 +8,13 @@ import wafsamba
from samba_utils import symlink
from optparse import SUPPRESS_HELP
def get_libc_version():
import ctypes
libc = ctypes.CDLL("libc.so.6")
gnu_get_libc_version = libc.gnu_get_libc_version
gnu_get_libc_version.restype = ctypes.c_char_p
return gnu_get_libc_version().decode()
phs = os.environ.get("PYTHONHASHSEED", None)
if phs != "1":
raise Errors.WafError('''PYTHONHASHSEED=1 missing! Don't use waf directly, use ./configure and make!''')
@ -288,6 +295,9 @@ Currently the only tested value is 'smbtorture,smbd/smbd' for Samba'''),
opt.add_option('--disable-python',
help='do not generate python modules',
action='store_true', dest='disable_python', default=False)
opt.add_option('--enable-rust',
help='build rust modules',
action='store_true', dest='enable_rust', default=False)
@Utils.run_once
@ -351,6 +361,13 @@ def configure(conf):
conf.env.AUTOCONF_PROGRAM_PREFIX = Options.options.AUTOCONF_PROGRAM_PREFIX
conf.env.disable_python = Options.options.disable_python
conf.env.enable_rust = Options.options.enable_rust
if Options.options.enable_rust:
glibc_vers = float('.'.join(get_libc_version().split('.')[:2]))
if glibc_vers < 2.32:
conf.fatal('--enable-rust cannot be specified with '
'glibc version %s' % glibc_vers)
conf.DEFINE('HAVE_RUST', '1')
if (conf.env.AUTOCONF_HOST and
conf.env.AUTOCONF_BUILD and

View File

@ -19,4 +19,5 @@ echo "
<!ENTITY pathconfig.SAMBA_DATADIR '\${prefix}/var/samba'>
<!ENTITY pathconfig.CTDB_DATADIR '\${prefix}/share/ctdb'>
<!ENTITY pathconfig.CONFIGFILE '\${prefix}/etc/smb.conf'>
<!ENTITY pathconfig.HIMMELBLAUD_HSM_PIN_PATH '\${prefix}/var/lib/himmelblaud/hsm-pin'>
"

View File

@ -0,0 +1,18 @@
<samba:parameter name="himmelblaud hello enabled"
context="G"
type="boolean"
xmlns:samba="http://www.samba.org/samba/DTD/samba-doc">
<description>
<para>This parameter controls Hello enrollment and authentication to
Azure Entra ID. By default, it is disabled to prevent security risks,
such as on hosts exposing the SSH port. Administrators should enable
this setting only when Hello enrollment is appropriate for their
environment.
</para>
</description>
<value type="default">no</value>
<value type="example">yes</value>
</samba:parameter>

View File

@ -0,0 +1,13 @@
<samba:parameter name="himmelblaud hsm pin path"
context="G"
type="string"
xmlns:samba="http://www.samba.org/samba/DTD/samba-doc">
<description>
<para>Specifies the file path where the HSM PIN is stored. This PIN is used
for unlocking TPM objects required for Azure Entra ID authentication. The HSM
PIN is critical for ensuring secure communication and authentication within
the Himmelblaud daemon.</para>
</description>
<value type="default">&pathconfig.HIMMELBLAUD_HSM_PIN_PATH;</value>
</samba:parameter>

View File

@ -0,0 +1,17 @@
<samba:parameter name="himmelblaud sfa fallback"
context="G"
type="boolean"
xmlns:samba="http://www.samba.org/samba/DTD/samba-doc">
<description>
<para>This parameter is designed to control whether Himmelblaud should fallback to
Single Factor Authentication (SFA) if Multi-Factor Authentication (MFA) isn't
available. This normally is possible during a short window during which MFA
enrollment is available to a new user.
</para>
</description>
<value type="default">no</value>
<value type="example">yes</value>
</samba:parameter>

View File

@ -107,3 +107,4 @@ DEFINE_DYN_CONFIG_PARAM(NTP_SIGND_SOCKET_DIR)
DEFINE_DYN_CONFIG_PARAM(PYTHONDIR)
DEFINE_DYN_CONFIG_PARAM(PYTHONARCHDIR)
DEFINE_DYN_CONFIG_PARAM(SCRIPTSBINDIR)
DEFINE_DYN_CONFIG_PARAM(HIMMELBLAUD_HSM_PIN_PATH)

View File

@ -58,3 +58,4 @@ DEFINE_DYN_CONFIG_PROTO(NTP_SIGND_SOCKET_DIR)
DEFINE_DYN_CONFIG_PROTO(PYTHONDIR)
DEFINE_DYN_CONFIG_PROTO(PYTHONARCHDIR)
DEFINE_DYN_CONFIG_PROTO(SCRIPTSBINDIR)
DEFINE_DYN_CONFIG_PROTO(HIMMELBLAUD_HSM_PIN_PATH)

View File

@ -285,6 +285,13 @@ dynconfig = {
'HELPTEXT': 'Where to put the smbpasswd file',
'DELAY': True,
},
'HIMMELBLAUD_HSM_PIN_PATH': {
'STD-PATH': '${LOCALSTATEDIR}/lib/himmelblaud/hsm-pin',
'FHS-PATH': '${LOCALSTATEDIR}/lib/himmelblaud/hsm-pin',
'OPTION': '--with-himmelblaud-hsm-pin-path',
'HELPTEXT': 'Where to store the hsm pin',
'DELAY': True,
},
}
def options(opt):

View File

@ -3165,6 +3165,17 @@ struct loadparm_context *loadparm_init(TALLOC_CTX *mem_ctx)
"acl claims evaluation",
"AD DC only");
/* Set the default Himmelblaud globals */
lpcfg_do_global_parameter(lp_ctx,
"himmelblaud hsm pin path",
get_dyn_HIMMELBLAUD_HSM_PIN_PATH());
lpcfg_do_global_parameter(lp_ctx,
"himmelblaud hello enabled",
"false");
lpcfg_do_global_parameter(lp_ctx,
"himmelblaud sfa fallback",
"false");
for (i = 0; parm_table[i].label; i++) {
if (!(lp_ctx->flags[i] & FLAG_CMDLINE)) {
lp_ctx->flags[i] |= FLAG_DEFAULT;

View File

@ -22,9 +22,9 @@ bld.SAMBA_GENERATOR('hresult_generated',
bld.SAMBA_GENERATOR('ntstatus_generated',
source='../../source4/scripting/bin/gen_ntstatus.py ntstatus_err_table.txt ../../source4/scripting/bin/gen_error_common.py',
target='ntstatus_gen.h nterr_gen.c py_ntstatus.c',
target='ntstatus_gen.h nterr_gen.c py_ntstatus.c ntstatus_gen.rs',
group='build_source',
rule='${PYTHON} ${SRC[0].abspath(env)} ${SRC[1].abspath(env)} ${TGT[0].abspath(env)} ${TGT[1].abspath(env)} ${TGT[2].abspath(env)}'
rule='${PYTHON} ${SRC[0].abspath(env)} ${SRC[1].abspath(env)} ${TGT[0].abspath(env)} ${TGT[1].abspath(env)} ${TGT[2].abspath(env)} ${TGT[3].abspath(env)}'
)
bld.SAMBA_GENERATOR('werror_generated',

View File

@ -309,6 +309,16 @@ static PyObject *py_is_ad_dc_built(PyObject *self,
#endif
}
static PyObject *py_is_rust_built(PyObject *self,
PyObject *Py_UNUSED(ignored))
{
#ifdef HAVE_RUST
Py_RETURN_TRUE;
#else
Py_RETURN_FALSE;
#endif
}
static PyObject *py_is_selftest_enabled(PyObject *self,
PyObject *Py_UNUSED(ignored))
{
@ -580,6 +590,8 @@ static PyMethodDef py_misc_methods[] = {
METH_NOARGS, "How many NDR internal tokens is too many for this build?" },
{ "get_burnt_commandline", (PyCFunction)py_get_burnt_commandline,
METH_VARARGS, "Return a redacted commandline to feed to setproctitle (None if no redaction required)" },
{ "is_rust_built", (PyCFunction)py_is_rust_built, METH_NOARGS,
"is Samba built with Rust?" },
{0}
};

View File

@ -432,6 +432,7 @@ is_ntvfs_fileserver_built = _glue.is_ntvfs_fileserver_built
is_heimdal_built = _glue.is_heimdal_built
is_ad_dc_built = _glue.is_ad_dc_built
is_selftest_enabled = _glue.is_selftest_enabled
is_rust_built = _glue.is_rust_built
NTSTATUSError = _glue.NTSTATUSError
HRESULTError = _glue.HRESULTError

View File

@ -0,0 +1,81 @@
# Unix SMB/CIFS implementation.
#
# Tests for Rust
#
# Copyright (C) David Mulder <dmulder@samba.org> 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/>.
#
"""Cargo tests for Rust sources"""
from samba.tests import TestCase, BlackboxProcessError
import os
from subprocess import Popen, PIPE
from samba import is_rust_built
class RustCargoTests(TestCase):
def setUp(self):
super().setUp()
# Locate the rust source directory
self.rust_dir = os.path.abspath(
os.path.join(
os.path.realpath(
os.path.dirname(__file__)
),
'../../../../rust'
)
)
# Locate the bin directory
self.target_dir = os.path.abspath(
os.path.join(
os.path.realpath(
os.path.dirname(__file__)
),
'../../..',
'default/rust',
)
)
def check_cargo_test(self, crate_toml):
# Execute the cargo test command
cmd = 'cargo test --target-dir=%s --manifest-path=%s' % (self.target_dir, crate_toml)
p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
stdoutdata, stderrdata = p.communicate()
retcode = p.returncode
if retcode != 0:
msg = "cargo test failed; return code %s" % retcode
raise BlackboxProcessError(retcode,
cmd,
stdoutdata.decode('utf-8'),
stderrdata.decode('utf-8'),
msg)
def test_rust(self):
if not is_rust_built():
self.skipTest('Cannot test Samba Rust if not built')
crates = []
for root, dirs, files in os.walk(self.rust_dir):
for file in files:
if os.path.basename(file) == 'Cargo.toml':
if root != self.rust_dir:
crates.append(os.path.join(root, file))
for crate_toml in crates:
with self.subTest(crate_toml):
self.check_cargo_test(crate_toml)

View File

@ -108,6 +108,8 @@ EXCLUDE_DIRS = {
'python/samba/tests/dcerpc',
'python/samba/tests/krb5',
'python/samba/tests/ndr',
'bin/default/rust/vendor/unicode-width/scripts',
'bin/default/rust/vendor/rustls-webpki/tests',
}

5
rust/.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "../bin/default/rust/vendor"

14
rust/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# ---> Rust
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
vendor/
tags

2899
rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
rust/Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[workspace.package]
edition = "2021"
license = "GPL-3.0-or-later"
homepage = "https://www.samba.org/"
version = "4.21.0"
[workspace]
resolver = "2"
members = [
"chelps", "config", "dbg", "himmelblaud", "idmap",
"nss", "ntstatus_gen", "pam",
"param", "sock", "talloc", "tdb", "version",
]
[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" }
libc = "0.2.155"
config = { path = "config" }

19
rust/build.rs Normal file
View File

@ -0,0 +1,19 @@
use std::env;
fn main() {
if let Some(vers) = version::samba_version_string() {
println!("cargo:rustc-env=CARGO_PKG_VERSION={}", vers);
}
println!(
"cargo:rustc-env=CARGO_PKG_VERSION_MAJOR={}",
version::SAMBA_VERSION_MAJOR
);
println!(
"cargo:rustc-env=CARGO_PKG_VERSION_MINOR={}",
version::SAMBA_VERSION_MINOR
);
println!(
"cargo:rustc-env=CARGO_PKG_VERSION_PATCH={}",
version::SAMBA_VERSION_RELEASE
);
}

8
rust/chelps/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "chelps"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[dependencies]

154
rust/chelps/src/lib.rs Normal file
View File

@ -0,0 +1,154 @@
/*
Unix SMB/CIFS implementation.
C conversion helper functions and macros
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 std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
pub unsafe fn wrap_c_char(input: *const c_char) -> Option<String> {
if input.is_null() {
return None;
}
let c_str = unsafe { CStr::from_ptr(input) };
match c_str.to_str() {
Ok(output) => Some(output.to_string()),
Err(_) => None,
}
}
pub fn wrap_string(input: &str) -> *mut c_char {
match CString::new(input.to_string()) {
Ok(msg) => msg.into_raw(),
Err(_) => ptr::null_mut(),
}
}
pub unsafe fn string_free(input: *mut c_char) {
if !input.is_null() {
unsafe {
let _ = CString::from_raw(input);
}
}
}
#[macro_export]
macro_rules! function {
() => {{
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let name = type_name_of(f);
let base_name = match name.rfind("::") {
Some(pos) => &name[..pos],
None => name,
};
let parts: Vec<&str> = base_name
.split("::")
.filter(|&p| p != "{{closure}}")
.collect();
parts.join("::")
}};
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::ptr;
#[test]
fn test_wrap_c_char_non_null() {
let original = "Hello, world!";
let c_string = CString::new(original).expect("CString::new failed");
let c_ptr = c_string.as_ptr();
let result = unsafe { wrap_c_char(c_ptr) };
assert_eq!(result, Some(original.to_string()));
}
#[test]
fn test_wrap_c_char_null() {
let result = unsafe { wrap_c_char(ptr::null()) };
assert!(result.is_none());
}
#[test]
fn test_wrap_c_char_invalid_utf8() {
let invalid_utf8 = vec![0xff, 0xff, 0xff, 0xff];
let c_string = CString::new(invalid_utf8).expect("CString::new failed");
let c_ptr = c_string.as_ptr();
let result = unsafe { wrap_c_char(c_ptr) };
assert!(result.is_none());
}
#[test]
fn test_wrap_string() {
let original = "Hello, world!";
let c_ptr = wrap_string(original);
let c_str = unsafe { CStr::from_ptr(c_ptr) };
let result = c_str.to_str().expect("CStr::to_str failed");
assert_eq!(result, original);
// Clean up the allocated memory
unsafe { string_free(c_ptr) };
}
#[test]
fn test_wrap_string_empty() {
let original = "";
let c_ptr = wrap_string(original);
let c_str = unsafe { CStr::from_ptr(c_ptr) };
let result = c_str.to_str().expect("CStr::to_str failed");
assert_eq!(result, original);
// Clean up the allocated memory
unsafe { string_free(c_ptr) };
}
#[test]
fn test_wrap_string_null_pointer() {
let c_ptr = wrap_string("\0");
assert!(c_ptr.is_null());
}
#[test]
fn test_string_free_null() {
unsafe { string_free(ptr::null_mut()) };
// No assertion needed, just ensuring no crash occurs
}
#[test]
fn test_string_free_non_null() {
let original = "Hello, world!";
let c_ptr = wrap_string(original);
unsafe { string_free(c_ptr) };
// No assertion needed, just ensuring the memory was freed without a crash
}
}

12
rust/config/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "config"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[dependencies]
libc.workspace = true
[build-dependencies]
bindgen = "0.69.4"

8
rust/config/additions.h Normal file
View File

@ -0,0 +1,8 @@
#include "../../bin/default/include/config.h"
#ifndef USING_SYSTEM_TDB
#define USING_SYSTEM_TDB 0
#endif
#ifndef USING_SYSTEM_TALLOC
#define USING_SYSTEM_TALLOC 0
#endif

20
rust/config/build.rs Normal file
View File

@ -0,0 +1,20 @@
use std::env;
use std::path::PathBuf;
fn main() {
let header = "../../bin/default/include/config.h";
println!("cargo:rerun-if-changed={}", header);
let additions_header = "./additions.h";
println!("cargo:rerun-if-changed={}", additions_header);
let bindings = bindgen::Builder::default()
.header(additions_header)
.header(header)
.generate()
.expect("Failed generating config bindings!");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}

52
rust/config/src/lib.rs Normal file
View File

@ -0,0 +1,52 @@
/*
Unix SMB/CIFS implementation.
Samba config imported into Rust
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/>.
*/
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(clippy::upper_case_acronyms)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_using_system_tdb() {
// This test just ensures that USING_SYSTEM_TDB is available from the
// config. None of the other options are really used at the moment.
assert!(
USING_SYSTEM_TDB == 0 || USING_SYSTEM_TDB == 1,
"Unexpected value for USING_SYSTEM_TDB: {}",
USING_SYSTEM_TDB
);
}
#[test]
fn test_using_system_talloc() {
assert!(
USING_SYSTEM_TALLOC == 0 || USING_SYSTEM_TALLOC == 1,
"Unexpected value for USING_SYSTEM_TALLOC: {}",
USING_SYSTEM_TALLOC
);
}
}

16
rust/dbg/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "dbg"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[build-dependencies]
bindgen = "0.69.4"
[dependencies]
chelps.workspace = true
[dev-dependencies]
tempfile = "3.12.0"
paste = "1.0.15"

25
rust/dbg/build.rs Normal file
View File

@ -0,0 +1,25 @@
use std::env;
use std::path::PathBuf;
fn main() {
let bindings = bindgen::Builder::default()
.header("../../lib/util/debug.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
let mut src_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
src_dir.push("../../bin/default/lib/util");
println!(
"cargo:rustc-link-search=native={}",
src_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=samba-debug-private-samba");
println!("cargo:rustc-link-lib=samba-util");
println!("cargo:rustc-env=LD_LIBRARY_PATH=../../bin/shared:../../bin/shared/private/");
}

217
rust/dbg/src/lib.rs Normal file
View File

@ -0,0 +1,217 @@
/*
Unix SMB/CIFS implementation.
Parameter loading functions
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/>.
*/
pub mod ffi {
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(clippy::upper_case_acronyms)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
pub const MAX_DEBUG_LEVEL: u32 = ffi::MAX_DEBUG_LEVEL;
pub const DBGLVL_ERR: u32 = ffi::DBGLVL_ERR;
pub const DBGLVL_WARNING: u32 = ffi::DBGLVL_WARNING;
pub const DBGLVL_NOTICE: u32 = ffi::DBGLVL_NOTICE;
pub const DBGLVL_INFO: u32 = ffi::DBGLVL_INFO;
pub const DBGLVL_DEBUG: u32 = ffi::DBGLVL_DEBUG;
pub const DEBUG_DEFAULT_STDERR: ffi::debug_logtype =
ffi::debug_logtype_DEBUG_DEFAULT_STDERR;
pub const DEBUG_DEFAULT_STDOUT: ffi::debug_logtype =
ffi::debug_logtype_DEBUG_DEFAULT_STDOUT;
pub const DEBUG_FILE: ffi::debug_logtype = ffi::debug_logtype_DEBUG_FILE;
pub const DEBUG_STDOUT: ffi::debug_logtype = ffi::debug_logtype_DEBUG_STDOUT;
pub const DEBUG_STDERR: ffi::debug_logtype = ffi::debug_logtype_DEBUG_STDERR;
pub const DEBUG_CALLBACK: ffi::debug_logtype =
ffi::debug_logtype_DEBUG_CALLBACK;
pub fn debug_set_logfile(name: &str) {
let name_cstr = chelps::wrap_string(name);
unsafe {
ffi::debug_set_logfile(name_cstr);
chelps::string_free(name_cstr);
}
}
pub fn setup_logging(prog_name: &str, new_logtype: ffi::debug_logtype) {
let prog_name_cstr = chelps::wrap_string(prog_name);
unsafe {
ffi::setup_logging(prog_name_cstr, new_logtype);
chelps::string_free(prog_name_cstr);
}
}
pub fn dbgflush() {
unsafe {
ffi::dbgflush();
}
}
#[macro_export]
macro_rules! debuglevel_set {
($level:expr) => {{
unsafe {
$crate::ffi::debuglevel_set_class(
$crate::ffi::DBGC_ALL as usize,
$level as i32,
)
}
}};
}
#[macro_export]
macro_rules! DBG_PREFIX {
($level:expr $(, $arg:expr)* $(,)?) => {{
if $level <= $crate::ffi::MAX_DEBUG_LEVEL {
let location = format!("{}:{}", file!(), line!());
let location_cstr = chelps::wrap_string(&location);
let function = chelps::function!();
let function_msg = format!("{}: ", function);
let function_cstr = chelps::wrap_string(&function);
let function_msg_cstr = chelps::wrap_string(&function_msg);
// Always append a newline to the debug, otherwise it won't flush
// to the log.
let msg = format!("{}\n", format!($($arg),*));
let msg_cstr = chelps::wrap_string(&msg);
unsafe {
let _ = $crate::ffi::debuglevel_get_class($crate::ffi::DBGC_CLASS as usize) >= ($level as i32)
&& $crate::ffi::dbghdrclass($level as i32,
$crate::ffi::DBGC_CLASS as i32,
location_cstr,
function_cstr)
&& $crate::ffi::dbgtext(function_msg_cstr)
&& $crate::ffi::dbgtext(msg_cstr);
chelps::string_free(location_cstr);
chelps::string_free(function_cstr);
chelps::string_free(function_msg_cstr);
chelps::string_free(msg_cstr);
}
}
}}
}
#[macro_export]
macro_rules! DBG_ERR {
($msg:expr $(, $arg:expr)* $(,)?) => {{
$crate::DBG_PREFIX!($crate::ffi::DBGLVL_ERR, $msg, $($arg),*)
}}
}
#[macro_export]
macro_rules! DBG_WARNING {
($msg:expr $(, $arg:expr)* $(,)?) => {{
$crate::DBG_PREFIX!($crate::ffi::DBGLVL_WARNING, $msg, $($arg),*)
}}
}
#[macro_export]
macro_rules! DBG_NOTICE {
($msg:expr $(, $arg:expr)* $(,)?) => {{
$crate::DBG_PREFIX!($crate::ffi::DBGLVL_NOTICE, $msg, $($arg),*)
}}
}
#[macro_export]
macro_rules! DBG_INFO {
($msg:expr $(, $arg:expr)* $(,)?) => {{
$crate::DBG_PREFIX!($crate::ffi::DBGLVL_INFO, $msg, $($arg),*)
}}
}
#[macro_export]
macro_rules! DBG_DEBUG {
($msg:expr $(, $arg:expr)* $(,)?) => {{
$crate::DBG_PREFIX!($crate::ffi::DBGLVL_DEBUG, $msg, $($arg),*)
}}
}
#[cfg(test)]
mod tests {
use super::*;
use paste::paste;
use std::fs::File;
use std::io::Read;
use tempfile::NamedTempFile;
#[test]
fn test_debug_constants() {
assert_eq!(MAX_DEBUG_LEVEL, ffi::MAX_DEBUG_LEVEL);
assert_eq!(DBGLVL_ERR, ffi::DBGLVL_ERR);
assert_eq!(DBGLVL_WARNING, ffi::DBGLVL_WARNING);
assert_eq!(DBGLVL_NOTICE, ffi::DBGLVL_NOTICE);
assert_eq!(DBGLVL_INFO, ffi::DBGLVL_INFO);
assert_eq!(DBGLVL_DEBUG, ffi::DBGLVL_DEBUG);
assert_eq!(
DEBUG_DEFAULT_STDERR,
ffi::debug_logtype_DEBUG_DEFAULT_STDERR
);
assert_eq!(
DEBUG_DEFAULT_STDOUT,
ffi::debug_logtype_DEBUG_DEFAULT_STDOUT
);
assert_eq!(DEBUG_FILE, ffi::debug_logtype_DEBUG_FILE);
assert_eq!(DEBUG_STDOUT, ffi::debug_logtype_DEBUG_STDOUT);
assert_eq!(DEBUG_STDERR, ffi::debug_logtype_DEBUG_STDERR);
assert_eq!(DEBUG_CALLBACK, ffi::debug_logtype_DEBUG_CALLBACK);
}
macro_rules! test_dbg_macro {
($level:ident) => {
paste! {
#[test]
fn [<test_dbg_ $level:lower _macro>]() {
let logfile = NamedTempFile::new().expect("Failed to create temporary file");
let logfile = logfile.path().to_str().unwrap();
setup_logging("test_program", DEBUG_FILE);
debug_set_logfile(logfile);
let logfile_output = concat!("This is a ", stringify!($level), " message");
debuglevel_set!([<DBGLVL_ $level:upper>]);
[<DBG_ $level:upper>]!("{}\n", logfile_output);
dbgflush();
let mut file = File::open(logfile).expect("Failed to open logfile");
let mut logfile_contents = String::new();
file.read_to_string(&mut logfile_contents)
.expect("Failed to read logfile");
assert!(
logfile_contents.contains(logfile_output),
"Test data missing from logfile: {}",
logfile_contents
);
}
}
};
}
test_dbg_macro!(DEBUG);
// Multiple re-inits of the debug env cause it to fail, so we can't
// reliably test all of these in one go.
//test_dbg_macro!(INFO);
//test_dbg_macro!(NOTICE);
//test_dbg_macro!(WARNING);
//test_dbg_macro!(ERR);
}

View File

@ -0,0 +1,32 @@
[package]
name = "himmelblaud"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[dependencies]
libhimmelblau = "0.2.9"
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 = { workspace = true }
talloc = { version = "4.21.0", path = "../talloc" }
[build-dependencies]
version = { path = "../version" }
[dev-dependencies]
tempfile = "3.12.0"

View File

@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-env=LD_LIBRARY_PATH=../../bin/shared:../../bin/shared/private/");
}

View File

@ -0,0 +1,658 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon cache
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 dbg::DBG_ERR;
use himmelblau::error::MsalError;
use himmelblau::graph::DirectoryObject;
use himmelblau::UserToken;
use kanidm_hsm_crypto::{
AuthValue, BoxedDynTpm, LoadableIdentityKey, LoadableMachineKey,
LoadableMsOapxbcRsaKey, Tpm,
};
use libc::uid_t;
use ntstatus_gen::*;
use serde::{Deserialize, Serialize};
use serde_json::{from_slice as json_from_slice, to_vec as json_to_vec};
use std::collections::HashSet;
use tdb::Tdb;
struct BasicCache {
tdb: Tdb,
}
impl BasicCache {
fn new(cache_path: &str) -> Result<Self, Box<NTSTATUS>> {
let tdb =
Tdb::open(cache_path, None, None, None, None).map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_FILE_INVALID)
})?;
Ok(BasicCache { tdb })
}
fn fetch_str(&self, key: &str) -> Option<String> {
let key = key.to_string().to_lowercase();
let exists = match self.tdb.exists(&key) {
Ok(exists) => exists,
Err(e) => {
DBG_ERR!("Failed to fetch {}: {:?}", key, e);
false
}
};
if exists {
match self.tdb.fetch(&key) {
Ok(val) => Some(val),
Err(e) => {
DBG_ERR!("Failed to fetch {}: {:?}", key, e);
None
}
}
} else {
None
}
}
fn fetch<'a, T>(&self, key: &str) -> Option<T>
where
T: for<'de> Deserialize<'de>,
{
let key = key.to_string().to_lowercase();
match self.fetch_str(&key) {
Some(val) => match json_from_slice::<T>(val.as_bytes()) {
Ok(res) => Some(res),
Err(e) => {
DBG_ERR!("Failed to decode {}: {:?}", key, e);
None
}
},
None => {
return None;
}
}
}
fn store_bytes(
&mut self,
key: &str,
val: &[u8],
) -> Result<(), Box<NTSTATUS>> {
let key = key.to_string().to_lowercase();
match self.tdb.transaction_start() {
Ok(start) => {
if !start {
DBG_ERR!("Failed to start the database transaction.");
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
}
Err(e) => {
DBG_ERR!("Failed to start the database transaction: {:?}", e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
let res = match self.tdb.store(&key, val, None) {
Ok(res) => Some(res),
Err(e) => {
DBG_ERR!("Unable to persist {}: {:?}", key, e);
None
}
};
let res = match res {
Some(res) => res,
None => {
let _ = self.tdb.transaction_cancel();
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
if !res {
DBG_ERR!("Unable to persist {}", key);
let _ = self.tdb.transaction_cancel();
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
let success = match self.tdb.transaction_commit() {
Ok(success) => success,
Err(e) => {
DBG_ERR!("Failed to commit the database transaction: {:?}", e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
if !success {
DBG_ERR!("Failed to commit the database transaction.");
let _ = self.tdb.transaction_cancel();
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
Ok(())
}
fn store<T>(&mut self, key: &str, val: T) -> Result<(), Box<NTSTATUS>>
where
T: Serialize,
{
let key = key.to_string().to_lowercase();
let val_bytes = match json_to_vec(&val) {
Ok(val_bytes) => val_bytes,
Err(e) => {
DBG_ERR!("Unable to serialize {}: {:?}", key, e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
self.store_bytes(&key, &val_bytes)
}
fn keys(&self) -> Result<Vec<String>, Box<NTSTATUS>> {
self.tdb.keys().map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct UserEntry {
pub(crate) upn: String,
pub(crate) uuid: String,
pub(crate) name: String,
}
impl TryFrom<&UserToken> for UserEntry {
type Error = MsalError;
fn try_from(token: &UserToken) -> Result<Self, Self::Error> {
Ok(UserEntry {
upn: token.spn()?,
uuid: token.uuid()?.to_string(),
name: token.id_token.name.clone(),
})
}
}
pub(crate) struct UserCache {
cache: BasicCache,
}
impl UserCache {
pub(crate) fn new(cache_path: &str) -> Result<Self, Box<NTSTATUS>> {
Ok(UserCache {
cache: BasicCache::new(cache_path)?,
})
}
pub(crate) fn fetch(&mut self, upn: &str) -> Option<UserEntry> {
self.cache.fetch::<UserEntry>(upn)
}
pub(crate) fn fetch_all(
&mut self,
) -> Result<Vec<UserEntry>, Box<NTSTATUS>> {
let keys = self.cache.keys()?;
let mut res = Vec::new();
for upn in keys {
let entry = match self.cache.fetch::<UserEntry>(&upn) {
Some(entry) => entry,
None => {
DBG_ERR!("Unable to fetch user {}", upn);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
res.push(entry);
}
Ok(res)
}
pub(crate) fn store(
&mut self,
entry: UserEntry,
) -> Result<(), Box<NTSTATUS>> {
let key = entry.upn.clone();
self.cache.store::<UserEntry>(&key, entry)
}
}
pub(crate) struct UidCache {
cache: BasicCache,
}
impl UidCache {
pub(crate) fn new(cache_path: &str) -> Result<Self, Box<NTSTATUS>> {
Ok(UidCache {
cache: BasicCache::new(cache_path)?,
})
}
pub(crate) fn store(
&mut self,
uid: uid_t,
upn: &str,
) -> Result<(), Box<NTSTATUS>> {
let key = format!("{}", uid);
let upn = upn.to_string().to_lowercase();
self.cache.store_bytes(&key, upn.as_bytes())
}
pub(crate) fn fetch(&mut self, uid: uid_t) -> Option<String> {
let key = format!("{}", uid);
self.cache.fetch_str(&key)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct GroupEntry {
pub(crate) uuid: String,
members: HashSet<String>,
}
impl From<DirectoryObject> for GroupEntry {
fn from(obj: DirectoryObject) -> Self {
GroupEntry {
uuid: obj.id.clone(),
members: HashSet::new(),
}
}
}
impl GroupEntry {
pub(crate) fn add_member(&mut self, member: &str) {
// Only ever use lowercase names, otherwise the group
// memberships will have duplicates.
self.members.insert(member.to_lowercase());
}
pub(crate) fn remove_member(&mut self, member: &str) {
// Only ever use lowercase names, otherwise the group
// memberships will have duplicates.
self.members.remove(&member.to_lowercase());
}
pub(crate) fn into_with_member(obj: DirectoryObject, member: &str) -> Self {
let mut g: GroupEntry = obj.into();
g.add_member(member);
g
}
pub(crate) fn members(&self) -> Vec<String> {
self.members.clone().into_iter().collect::<Vec<String>>()
}
}
impl GroupEntry {
pub fn new(uuid: &str) -> Self {
GroupEntry {
uuid: uuid.to_string(),
members: HashSet::new(),
}
}
}
pub(crate) struct GroupCache {
cache: BasicCache,
}
impl GroupCache {
pub(crate) fn new(cache_path: &str) -> Result<Self, Box<NTSTATUS>> {
Ok(GroupCache {
cache: BasicCache::new(cache_path)?,
})
}
pub(crate) fn fetch(&mut self, uuid: &str) -> Option<GroupEntry> {
self.cache.fetch::<GroupEntry>(uuid)
}
pub(crate) fn fetch_all(
&mut self,
) -> Result<Vec<GroupEntry>, Box<NTSTATUS>> {
let keys = self.cache.keys()?;
let mut res = Vec::new();
for uuid in keys {
let entry = match self.cache.fetch::<GroupEntry>(&uuid) {
Some(entry) => entry,
None => {
DBG_ERR!("Unable to fetch group {}", uuid);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
res.push(entry);
}
Ok(res)
}
pub(crate) fn merge_groups(
&mut self,
member: &str,
mut entries: Vec<GroupEntry>,
) -> Result<(), Box<NTSTATUS>> {
// We need to ensure the member is removed from non-intersecting
// groups, otherwise we don't honor group membership removals.
let group_uuids: HashSet<String> = entries
.clone()
.into_iter()
.map(|g| g.uuid.clone())
.collect();
let existing_group_uuids = {
let cache = &self.cache;
match cache.keys() {
Ok(keys) => keys,
Err(e) => {
DBG_ERR!("Unable to fetch groups: {:?}", e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
}
};
let existing_group_uuids: HashSet<String> =
existing_group_uuids.into_iter().collect();
let difference: HashSet<String> = existing_group_uuids
.difference(&group_uuids)
.cloned()
.collect();
for group_uuid in &difference {
if let Some(mut group) =
self.cache.fetch::<GroupEntry>(&group_uuid).clone()
{
group.remove_member(member);
if let Err(e) =
self.cache.store::<GroupEntry>(&group.uuid.clone(), group)
{
DBG_ERR!("Unable to store membership change: {:?}", e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
}
}
// Ensure the member is added to the listed groups
for group in &mut entries {
group.add_member(member);
}
// Now add the new entries, merging with existing memberships
for group in entries {
match self.cache.fetch::<GroupEntry>(&group.uuid) {
Some(mut existing_group) => {
// Merge with an existing entry
existing_group.add_member(member);
if let Err(e) = self.cache.store::<GroupEntry>(
&existing_group.uuid.clone(),
existing_group,
) {
DBG_ERR!("Unable to store membership change: {:?}", e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
}
None => {
if let Err(e) = self
.cache
.store::<GroupEntry>(&group.uuid.clone(), group)
{
DBG_ERR!("Unable to store membership change: {:?}", e);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
}
}
}
Ok(())
}
}
pub(crate) struct PrivateCache {
cache: BasicCache,
}
impl PrivateCache {
pub(crate) fn new(cache_path: &str) -> Result<Self, Box<NTSTATUS>> {
Ok(PrivateCache {
cache: BasicCache::new(cache_path)?,
})
}
pub(crate) fn loadable_machine_key_fetch_or_create(
&mut self,
hsm: &mut BoxedDynTpm,
auth_value: &AuthValue,
) -> Result<LoadableMachineKey, Box<NTSTATUS>> {
match self
.cache
.fetch::<LoadableMachineKey>("loadable_machine_key")
{
Some(loadable_machine_key) => Ok(loadable_machine_key),
None => {
// No machine key found - create one, and store it.
let loadable_machine_key =
match hsm.machine_key_create(&auth_value) {
Ok(loadable_machine_key) => loadable_machine_key,
Err(e) => {
DBG_ERR!(
"Unable to create hsm loadable \
machine key: {:?}",
e
);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
};
self.cache.store::<LoadableMachineKey>(
"loadable_machine_key",
loadable_machine_key.clone(),
)?;
Ok(loadable_machine_key)
}
}
}
pub(crate) fn loadable_transport_key_fetch(
&mut self,
realm: &str,
) -> Option<LoadableMsOapxbcRsaKey> {
let transport_key_tag = format!("{}/transport", realm);
self.cache
.fetch::<LoadableMsOapxbcRsaKey>(&transport_key_tag)
}
pub(crate) fn loadable_cert_key_fetch(
&mut self,
realm: &str,
) -> Option<LoadableIdentityKey> {
let cert_key_tag = format!("{}/certificate", realm);
self.cache.fetch::<LoadableIdentityKey>(&cert_key_tag)
}
pub(crate) fn loadable_hello_key_fetch(
&mut self,
account_id: &str,
) -> Option<LoadableIdentityKey> {
let hello_key_tag = format!("{}/hello", account_id);
self.cache.fetch::<LoadableIdentityKey>(&hello_key_tag)
}
pub(crate) fn loadable_cert_key_store(
&mut self,
realm: &str,
cert_key: LoadableIdentityKey,
) -> Result<(), Box<NTSTATUS>> {
let cert_key_tag = format!("{}/certificate", realm);
self.cache
.store::<LoadableIdentityKey>(&cert_key_tag, cert_key)
}
pub(crate) fn loadable_hello_key_store(
&mut self,
account_id: &str,
hello_key: LoadableIdentityKey,
) -> Result<(), Box<NTSTATUS>> {
let hello_key_tag = format!("{}/hello", account_id);
self.cache
.store::<LoadableIdentityKey>(&hello_key_tag, hello_key)
}
pub(crate) fn loadable_transport_key_store(
&mut self,
realm: &str,
transport_key: LoadableMsOapxbcRsaKey,
) -> Result<(), Box<NTSTATUS>> {
let transport_key_tag = format!("{}/transport", realm);
self.cache
.store::<LoadableMsOapxbcRsaKey>(&transport_key_tag, transport_key)
}
pub(crate) fn device_id(&mut self, realm: &str) -> Option<String> {
let device_id_tag = format!("{}/device_id", realm);
self.cache.fetch_str(&device_id_tag)
}
pub(crate) fn device_id_store(
&mut self,
realm: &str,
device_id: &str,
) -> Result<(), Box<NTSTATUS>> {
let device_id_tag = format!("{}/device_id", realm);
self.cache.store_bytes(&device_id_tag, device_id.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use kanidm_hsm_crypto::soft::SoftTpm;
use std::str::FromStr;
use tempfile::tempdir;
#[test]
fn test_basic_cache_new() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let cache = BasicCache::new(cache_path.to_str().unwrap());
assert!(cache.is_ok());
}
#[test]
fn test_basic_cache_store_fetch_str() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let mut cache = BasicCache::new(cache_path.to_str().unwrap()).unwrap();
let key = "test_key";
let value = "test_value";
cache.store_bytes(key, value.as_bytes()).unwrap();
let fetched_value = cache.fetch_str(key).unwrap();
assert_eq!(fetched_value, value);
}
#[test]
fn test_basic_cache_store_fetch() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let mut cache = BasicCache::new(cache_path.to_str().unwrap()).unwrap();
let key = "test_key";
let value = UserEntry {
upn: "user@test.com".to_string(),
uuid: "f63a43c7-b783-4da9-acb4-89f8ebfc49e9".to_string(),
name: "Test User".to_string(),
};
cache.store(key, &value).unwrap();
let fetched_value: Option<UserEntry> = cache.fetch(key);
assert!(fetched_value.is_some());
let fetched_value = fetched_value.unwrap();
assert_eq!(fetched_value.upn, value.upn);
assert_eq!(fetched_value.uuid, value.uuid);
assert_eq!(fetched_value.name, value.name);
}
#[test]
fn test_user_cache_store_fetch() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let mut cache = UserCache::new(cache_path.to_str().unwrap()).unwrap();
let entry = UserEntry {
upn: "user@test.com".to_string(),
uuid: "f63a43c7-b783-4da9-acb4-89f8ebfc49e9".to_string(),
name: "Test User".to_string(),
};
cache.store(entry.clone()).unwrap();
let fetched_entry = cache.fetch(&entry.upn);
assert!(fetched_entry.is_some());
let fetched_entry = fetched_entry.unwrap();
assert_eq!(fetched_entry.upn, entry.upn);
assert_eq!(fetched_entry.uuid, entry.uuid);
assert_eq!(fetched_entry.name, entry.name);
}
#[test]
fn test_uid_cache_store_fetch() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let mut cache = UidCache::new(cache_path.to_str().unwrap()).unwrap();
let uid: uid_t = 1000;
let upn = "user@test.com";
cache.store(uid, upn).unwrap();
let fetched_upn = cache.fetch(uid);
assert!(fetched_upn.is_some());
assert_eq!(fetched_upn.unwrap(), upn);
}
#[test]
fn test_group_cache_store_fetch() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let mut cache = GroupCache::new(cache_path.to_str().unwrap()).unwrap();
let mut group = GroupEntry {
uuid: "5f8be63a-a379-4324-9f42-9ea40bed9d7f".to_string(),
members: HashSet::new(),
};
group.add_member("user@test.com");
cache.cache.store(&group.uuid, &group).unwrap();
let fetched_group = cache.fetch(&group.uuid);
assert!(fetched_group.is_some());
let fetched_group = fetched_group.unwrap();
assert_eq!(fetched_group.uuid, group.uuid);
assert!(fetched_group.members.contains("user@test.com"));
}
#[test]
fn test_private_cache_loadable_machine_key_fetch_or_create() {
let dir = tempdir().unwrap();
let cache_path = dir.path().join("test.tdb");
let mut cache =
PrivateCache::new(cache_path.to_str().unwrap()).unwrap();
let mut hsm = BoxedDynTpm::new(SoftTpm::new());
let auth_str = AuthValue::generate().expect("Failed to create hex pin");
let auth_value = AuthValue::from_str(&auth_str)
.expect("Unable to create auth value");
let result =
cache.loadable_machine_key_fetch_or_create(&mut hsm, &auth_value);
assert!(result.is_ok());
let fetched_key = cache
.cache
.fetch::<LoadableMachineKey>("loadable_machine_key");
assert!(fetched_key.is_some());
}
}

View File

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

View File

@ -0,0 +1,295 @@
/*
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, UidCache, UserCache};
#[cfg(not(test))]
use crate::himmelblaud::himmelblaud_pam_auth::AuthSession;
use bytes::{BufMut, BytesMut};
use dbg::{DBG_DEBUG, DBG_ERR, DBG_WARNING};
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::{PamAuthResponse, 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};
#[cfg(not(test))]
pub(crate) struct Resolver {
realm: String,
tenant_id: String,
lp: LoadParm,
idmap: Idmap,
graph: Graph,
pcache: PrivateCache,
user_cache: UserCache,
uid_cache: UidCache,
group_cache: GroupCache,
hsm: Mutex<BoxedDynTpm>,
machine_key: MachineKey,
client: Arc<Mutex<BrokerClientApplication>>,
}
#[cfg(not(test))]
impl Resolver {
pub(crate) fn new(
realm: &str,
tenant_id: &str,
lp: LoadParm,
idmap: Idmap,
graph: Graph,
pcache: PrivateCache,
user_cache: UserCache,
uid_cache: UidCache,
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,
uid_cache,
group_cache,
hsm: Mutex::new(hsm),
machine_key,
client: Arc::new(Mutex::new(client)),
}
}
}
// The test environment is unable to communicate with Entra ID, therefore
// we alter the resolver to only test the cache interactions.
#[cfg(test)]
pub(crate) struct Resolver {
realm: String,
tenant_id: String,
lp: LoadParm,
idmap: Idmap,
pcache: PrivateCache,
user_cache: UserCache,
uid_cache: UidCache,
group_cache: GroupCache,
}
#[cfg(test)]
impl Resolver {
pub(crate) fn new(
realm: &str,
tenant_id: &str,
lp: LoadParm,
idmap: Idmap,
pcache: PrivateCache,
user_cache: UserCache,
uid_cache: UidCache,
group_cache: GroupCache,
) -> Self {
Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
}
}
}
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());
#[cfg(not(test))]
let mut pam_auth_session_state = None;
while let Some(Ok(req)) = reqs.next().await {
let mut resolver = resolver.lock().await;
let resp = match req {
#[cfg(not(test))]
Request::PamAuthenticateInit(account_id) => {
DBG_DEBUG!("pam authenticate init");
match &pam_auth_session_state {
Some(_) => {
DBG_WARNING!(
"Attempt to init \
auth session while current \
session is active"
);
pam_auth_session_state = None;
Response::Error
}
None => {
let (auth_session, resp) =
resolver.pam_auth_init(&account_id)?;
pam_auth_session_state = Some(auth_session);
resp
}
}
}
#[cfg(not(test))]
Request::PamAuthenticateStep(pam_next_req) => {
DBG_DEBUG!("pam authenticate step");
match &mut pam_auth_session_state {
Some(AuthSession::InProgress {
account_id,
cred_handler,
}) => {
let resp = resolver
.pam_auth_step(
account_id,
cred_handler,
pam_next_req,
)
.await?;
match resp {
Response::PamAuthStepResponse(
PamAuthResponse::Success,
) => {
pam_auth_session_state =
Some(AuthSession::Success);
}
Response::PamAuthStepResponse(
PamAuthResponse::Denied,
) => {
pam_auth_session_state =
Some(AuthSession::Denied);
}
_ => {}
}
resp
}
_ => {
DBG_WARNING!(
"Attempt to \
continue auth session \
while current session is \
inactive"
);
Response::Error
}
}
}
Request::NssAccounts => resolver.getpwent().await?,
Request::NssAccountByName(account_id) => {
resolver.getpwnam(&account_id).await?
}
Request::NssAccountByUid(uid) => resolver.getpwuid(uid).await?,
Request::NssGroups => resolver.getgrent().await?,
Request::NssGroupByName(grp_id) => {
resolver.getgrnam(&grp_id).await?
}
Request::NssGroupByGid(gid) => resolver.getgrgid(gid).await?,
#[cfg(not(test))]
Request::PamAccountAllowed(account_id) => {
resolver.pam_acct_mgmt(&account_id).await?
}
Request::PamAccountBeginSession(_account_id) => Response::Success,
#[cfg(test)]
_ => Response::Error,
};
reqs.send(resp).await?;
reqs.flush().await?;
DBG_DEBUG!("flushed response!");
}
DBG_DEBUG!("Disconnecting client ...");
Ok(())
}
mod himmelblaud_getgrent;
mod himmelblaud_getgrgid;
mod himmelblaud_getgrnam;
mod himmelblaud_getpwent;
mod himmelblaud_getpwnam;
mod himmelblaud_getpwuid;
#[cfg(not(test))]
mod himmelblaud_pam_acct_mgmt;
#[cfg(not(test))]
mod himmelblaud_pam_auth;

View File

@ -0,0 +1,154 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for nss getgrent
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::himmelblaud::Resolver;
use dbg::DBG_ERR;
use ntstatus_gen::*;
use sock::{Group, Response};
impl Resolver {
pub(crate) async fn getgrent(&mut self) -> Result<Response, Box<NTSTATUS>> {
let group_entries = self.group_cache.fetch_all()?;
let mut res = Vec::new();
for entry in group_entries {
let name = entry.uuid.clone();
let gid = self
.idmap
.gen_to_unix(&self.tenant_id, &entry.uuid)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NO_SUCH_GROUP)
})?;
let group = Group {
name,
passwd: "x".to_string(),
gid,
members: entry.members(),
};
res.push(group);
}
Ok(Response::NssGroups(res))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::GroupEntry;
use crate::{GroupCache, PrivateCache, UidCache, UserCache};
use idmap::Idmap;
use param::LoadParm;
use std::collections::HashSet;
use tempfile::tempdir;
#[tokio::test]
async fn test_getgrent() {
// Create a temporary directory for the cache
let dir = tempdir().unwrap();
// Initialize the caches
let private_cache_path = dir
.path()
.join("himmelblau.tdb")
.to_str()
.unwrap()
.to_string();
let pcache = PrivateCache::new(&private_cache_path).unwrap();
let user_cache_path = dir
.path()
.join("himmelblau_users.tdb")
.to_str()
.unwrap()
.to_string();
let user_cache = UserCache::new(&user_cache_path).unwrap();
let uid_cache_path = dir
.path()
.join("uid_cache.tdb")
.to_str()
.unwrap()
.to_string();
let uid_cache = UidCache::new(&uid_cache_path).unwrap();
let group_cache_path = dir
.path()
.join("himmelblau_groups.tdb")
.to_str()
.unwrap()
.to_string();
let mut group_cache = GroupCache::new(&group_cache_path).unwrap();
// Insert dummy GroupEntries into the cache
let group_uuid1 = "c490c3ea-fd98-4d45-b6aa-2a3520f804fa";
let group_uuid2 = "f7a51b58-84de-42a3-b5b1-967b17c04f89";
let dummy_group1 = GroupEntry::new(group_uuid1);
let dummy_group2 = GroupEntry::new(group_uuid2);
group_cache
.merge_groups("user1@test.com", vec![dummy_group1.clone()])
.unwrap();
group_cache
.merge_groups("user2@test.com", vec![dummy_group2.clone()])
.unwrap();
// Initialize the Idmap with dummy configuration
let realm = "test.com";
let tenant_id = "89a61bb7-d1b9-4356-a1e0-75d88e06f14e";
let mut idmap = Idmap::new().unwrap();
idmap
.add_gen_domain(realm, tenant_id, (1000, 2000))
.unwrap();
// Initialize dummy configuration
let lp = LoadParm::new(None).expect("Failed loading default config");
// Initialize the Resolver
let mut resolver = Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
};
// Test the getgrent function
let result = resolver.getgrent().await.unwrap();
match result {
Response::NssGroups(mut groups) => {
groups.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(groups.len(), 2);
let group1 = &groups[0];
assert_eq!(group1.name, dummy_group1.uuid);
assert_eq!(group1.gid, 1388);
assert_eq!(group1.members, vec!["user1@test.com".to_string()]);
let group2 = &groups[1];
assert_eq!(group2.name, dummy_group2.uuid);
assert_eq!(group2.gid, 1593);
assert_eq!(group2.members, vec!["user2@test.com".to_string()]);
}
other => {
panic!("Expected NssGroups with a list of groups: {:?}", other)
}
}
}
}

View File

@ -0,0 +1,147 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for nss getgrgid
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::himmelblaud::Resolver;
use libc::gid_t;
use ntstatus_gen::NTSTATUS;
use sock::{Group, Response};
impl Resolver {
pub(crate) async fn getgrgid(
&mut self,
gid: gid_t,
) -> Result<Response, Box<NTSTATUS>> {
if let Some(uuid) = self.uid_cache.fetch(gid) {
if let Some(entry) = self.group_cache.fetch(&uuid) {
return Ok(Response::NssGroup(Some(Group {
name: entry.uuid.clone(),
passwd: "x".to_string(),
gid,
members: entry.members(),
})));
}
}
Ok(Response::NssGroup(None))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::GroupEntry;
use crate::{GroupCache, PrivateCache, UidCache, UserCache};
use idmap::Idmap;
use param::LoadParm;
use std::collections::HashSet;
use tempfile::tempdir;
#[tokio::test]
async fn test_getgrgid() {
// Create a temporary directory for the cache
let dir = tempdir().unwrap();
// Initialize the caches
let private_cache_path = dir
.path()
.join("himmelblau.tdb")
.to_str()
.unwrap()
.to_string();
let pcache = PrivateCache::new(&private_cache_path).unwrap();
let user_cache_path = dir
.path()
.join("himmelblau_users.tdb")
.to_str()
.unwrap()
.to_string();
let user_cache = UserCache::new(&user_cache_path).unwrap();
let uid_cache_path = dir
.path()
.join("uid_cache.tdb")
.to_str()
.unwrap()
.to_string();
let mut uid_cache = UidCache::new(&uid_cache_path).unwrap();
let group_cache_path = dir
.path()
.join("himmelblau_groups.tdb")
.to_str()
.unwrap()
.to_string();
let mut group_cache = GroupCache::new(&group_cache_path).unwrap();
// Initialize the Idmap with dummy configuration
let realm = "test.com";
let tenant_id = "89a61bb7-d1b9-4356-a1e0-75d88e06f14e";
let mut idmap = Idmap::new().unwrap();
idmap
.add_gen_domain(realm, tenant_id, (1000, 2000))
.unwrap();
// Insert a dummy GroupEntry into the cache
let group_uuid = "c490c3ea-fd98-4d45-b6aa-2a3520f804fa".to_string();
let dummy_gid = idmap
.gen_to_unix(tenant_id, &group_uuid)
.expect("Failed to map group gid");
// Store the calculated gid -> uuid map in the cache
uid_cache
.store(dummy_gid, &group_uuid)
.expect("Failed to store group gid");
let dummy_group = GroupEntry::new(&group_uuid);
group_cache
.merge_groups("user1@test.com", vec![dummy_group.clone()])
.unwrap();
// Initialize dummy configuration
let lp = LoadParm::new(None).expect("Failed loading default config");
// Initialize the Resolver
let mut resolver = Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
};
// Test the getgrgid function with a gid that exists
let result = resolver.getgrgid(dummy_gid).await.unwrap();
match result {
Response::NssGroup(Some(group)) => {
assert_eq!(group.name, dummy_group.uuid);
assert_eq!(group.gid, dummy_gid);
assert_eq!(group.members, vec!["user1@test.com".to_string()]);
}
other => panic!("Expected NssGroup with Some(group): {:?}", other),
}
// Test the getgrgid function with a gid that does not exist
let nonexistent_gid: gid_t = 1600;
let result = resolver.getgrgid(nonexistent_gid).await.unwrap();
match result {
Response::NssGroup(None) => {} // This is the expected result
_ => panic!("Expected NssGroup with None"),
}
}
}

View File

@ -0,0 +1,159 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for nss getgrnam
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::himmelblaud::Resolver;
use dbg::DBG_ERR;
use ntstatus_gen::*;
use sock::{Group, Response};
impl Resolver {
pub(crate) async fn getgrnam(
&mut self,
grp_id: &str,
) -> Result<Response, Box<NTSTATUS>> {
let entry = match self.group_cache.fetch(grp_id) {
Some(entry) => entry,
None => return Ok(Response::NssGroup(None)),
};
let gid = self
.idmap
.gen_to_unix(&self.tenant_id, &entry.uuid)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_INVALID_TOKEN)
})?;
// Store the calculated gid -> uuid map in the cache
self.uid_cache.store(gid, &entry.uuid)?;
let group = Group {
name: entry.uuid.clone(),
passwd: "x".to_string(),
gid,
members: entry.members(),
};
return Ok(Response::NssGroup(Some(group)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::GroupEntry;
use crate::{GroupCache, PrivateCache, UidCache, UserCache};
use idmap::Idmap;
use param::LoadParm;
use std::collections::HashSet;
use tempfile::tempdir;
#[tokio::test]
async fn test_getgrnam() {
// Create a temporary directory for the cache
let dir = tempdir().unwrap();
// Initialize the caches
let private_cache_path = dir
.path()
.join("himmelblau.tdb")
.to_str()
.unwrap()
.to_string();
let pcache = PrivateCache::new(&private_cache_path).unwrap();
let user_cache_path = dir
.path()
.join("himmelblau_users.tdb")
.to_str()
.unwrap()
.to_string();
let user_cache = UserCache::new(&user_cache_path).unwrap();
let uid_cache_path = dir
.path()
.join("uid_cache.tdb")
.to_str()
.unwrap()
.to_string();
let uid_cache = UidCache::new(&uid_cache_path).unwrap();
let group_cache_path = dir
.path()
.join("himmelblau_groups.tdb")
.to_str()
.unwrap()
.to_string();
let mut group_cache = GroupCache::new(&group_cache_path).unwrap();
// Insert a dummy GroupEntry into the cache
let group_uuid = "c490c3ea-fd98-4d45-b6aa-2a3520f804fa";
let dummy_group = GroupEntry::new(group_uuid);
group_cache
.merge_groups("user1@test.com", vec![dummy_group.clone()])
.unwrap();
group_cache
.merge_groups("user2@test.com", vec![dummy_group.clone()])
.unwrap();
// Initialize the Idmap with dummy configuration
let realm = "test.com";
let tenant_id = "89a61bb7-d1b9-4356-a1e0-75d88e06f14e";
let mut idmap = Idmap::new().unwrap();
idmap
.add_gen_domain(realm, tenant_id, (1000, 2000))
.unwrap();
// Initialize dummy configuration
let lp = LoadParm::new(None).expect("Failed loading default config");
// Initialize the Resolver
let mut resolver = Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
};
// Test the getgrnam function with a group that exists
let result = resolver.getgrnam(group_uuid).await.unwrap();
match result {
Response::NssGroup(Some(mut group)) => {
group.members.sort();
assert_eq!(group.name, dummy_group.uuid);
assert_eq!(group.gid, 1388);
assert_eq!(
group.members,
vec![
"user1@test.com".to_string(),
"user2@test.com".to_string()
]
);
}
other => panic!("Expected NssGroup with Some(group): {:?}", other),
}
// Test the getgrnam function with a group that does not exist
let nonexistent_group_uuid = "2ea8f1d4-1b94-4003-865b-cb247a8a1f5d";
let result = resolver.getgrnam(nonexistent_group_uuid).await.unwrap();
match result {
Response::NssGroup(None) => {} // This is the expected result
_ => panic!("Expected NssGroup with None"),
}
}
}

View File

@ -0,0 +1,210 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for nss getpwent
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::himmelblaud::Resolver;
use crate::utils::split_username;
use dbg::DBG_ERR;
use ntstatus_gen::*;
use sock::{Passwd, Response};
impl Resolver {
pub(crate) async fn getpwent(&mut self) -> Result<Response, Box<NTSTATUS>> {
let user_entries = self.user_cache.fetch_all()?;
let template_homedir = self
.lp
.template_homedir()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?
.ok_or_else(|| {
DBG_ERR!("Failed to discover template homedir. Is it set?");
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?;
let shell = self
.lp
.template_shell()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?
.ok_or_else(|| {
DBG_ERR!("Failed to discover template shell. Is it set?");
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?;
let mut res = Vec::new();
for entry in user_entries {
let uid = self
.idmap
.gen_to_unix(&self.tenant_id, &entry.upn)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_INVALID_TOKEN)
})?;
let upn = entry.upn.clone();
let (cn, domain) = match split_username(&upn) {
Ok(res) => res,
Err(e) => {
DBG_ERR!(
"Failed to parse user upn '{}': {:?}",
&entry.upn,
e
);
return Err(Box::new(
NT_STATUS_INVALID_USER_PRINCIPAL_NAME,
));
}
};
let homedir = template_homedir
.clone()
.replace("%D", &domain)
.replace("%U", &cn);
let passwd = Passwd {
name: entry.upn.clone(),
passwd: "x".to_string(),
uid,
gid: uid,
gecos: entry.name,
dir: homedir,
shell: shell.clone(),
};
res.push(passwd);
}
Ok(Response::NssAccounts(res))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::UserEntry;
use crate::{GroupCache, PrivateCache, UidCache, UserCache};
use idmap::Idmap;
use param::LoadParm;
use std::collections::HashSet;
use tempfile::tempdir;
#[tokio::test]
async fn test_getpwent() {
// Create a temporary directory for the cache
let dir = tempdir().unwrap();
// Initialize the caches
let private_cache_path = dir
.path()
.join("himmelblau.tdb")
.to_str()
.unwrap()
.to_string();
let pcache = PrivateCache::new(&private_cache_path).unwrap();
let user_cache_path = dir
.path()
.join("himmelblau_users.tdb")
.to_str()
.unwrap()
.to_string();
let mut user_cache = UserCache::new(&user_cache_path).unwrap();
let uid_cache_path = dir
.path()
.join("uid_cache.tdb")
.to_str()
.unwrap()
.to_string();
let uid_cache = UidCache::new(&uid_cache_path).unwrap();
let group_cache_path = dir
.path()
.join("himmelblau_groups.tdb")
.to_str()
.unwrap()
.to_string();
let group_cache = GroupCache::new(&group_cache_path).unwrap();
// Insert dummy UserEntrys into the cache
let dummy_user = UserEntry {
upn: "user1@test.com".to_string(),
uuid: "731e9af3-668d-4033-afd1-9f09b9120cc7".to_string(),
name: "User One".to_string(),
};
user_cache
.store(dummy_user.clone())
.expect("Failed storing user in cache");
let dummy_user2 = UserEntry {
upn: "user2@test.com".to_string(),
uuid: "7be6c0c5-5763-4633-aecf-f8c460b338fd".to_string(),
name: "User Two".to_string(),
};
user_cache
.store(dummy_user2.clone())
.expect("Failed storing user in cache");
// Initialize the Idmap with dummy configuration
let realm = "test.com";
let tenant_id = "89a61bb7-d1b9-4356-a1e0-75d88e06f14e";
let mut idmap = Idmap::new().unwrap();
idmap
.add_gen_domain(realm, tenant_id, (1000, 2000))
.unwrap();
// Initialize dummy configuration for LoadParm
let lp = LoadParm::new(None).expect("Failed loading default config");
// Initialize the Resolver
let mut resolver = Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
};
// Test the getpwent function
let result = resolver.getpwent().await.unwrap();
match result {
Response::NssAccounts(accounts) => {
assert_eq!(accounts.len(), 2);
let account1 = &accounts[0];
assert_eq!(account1.name, dummy_user.upn);
assert_eq!(account1.uid, 1316);
assert_eq!(account1.gid, 1316);
assert_eq!(account1.gecos, dummy_user.name);
assert_eq!(account1.dir, "/home/test.com/user1");
assert_eq!(account1.shell, "/bin/false");
let account2 = &accounts[1];
assert_eq!(account2.name, dummy_user2.upn);
assert_eq!(account2.uid, 1671);
assert_eq!(account2.gid, 1671);
assert_eq!(account2.gecos, dummy_user2.name);
assert_eq!(account2.dir, "/home/test.com/user2");
assert_eq!(account2.shell, "/bin/false");
}
other => panic!(
"Expected NssAccounts with a list of accounts: {:?}",
other
),
}
}
}

View File

@ -0,0 +1,232 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for nss getpwnam
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::GroupEntry;
use crate::himmelblaud::Resolver;
use crate::utils::split_username;
use dbg::{DBG_ERR, DBG_WARNING};
use ntstatus_gen::*;
use sock::{Passwd, Response};
impl Resolver {
pub(crate) fn create_passwd_from_upn(
&mut self,
upn: &str,
gecos: &str,
) -> Result<Passwd, Box<NTSTATUS>> {
let template_homedir = self
.lp
.template_homedir()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?
.ok_or_else(|| {
DBG_ERR!("Failed to discover template homedir. Is it set?");
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?;
let shell = self
.lp
.template_shell()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?
.ok_or_else(|| {
DBG_ERR!("Failed to discover template shell. Is it set?");
Box::new(NT_STATUS_NOT_A_DIRECTORY)
})?;
let uid =
self.idmap.gen_to_unix(&self.tenant_id, &upn).map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_INVALID_TOKEN)
})?;
// Store the calculated uid -> upn map in the cache
self.uid_cache.store(uid, &upn)?;
// Store the primary group (which is a fake group matching the user upn)
let mut group = GroupEntry::new(upn);
group.add_member(upn);
self.group_cache.merge_groups(upn, vec![group])?;
let (cn, domain) = match split_username(&upn) {
Ok(res) => res,
Err(e) => {
DBG_ERR!("Failed to parse user upn '{}': {:?}", upn, e);
return Err(Box::new(NT_STATUS_INVALID_USER_PRINCIPAL_NAME));
}
};
let homedir = template_homedir
.clone()
.replace("%D", &domain)
.replace("%U", &cn);
let passwd = Passwd {
name: upn.to_string(),
passwd: "x".to_string(),
uid,
gid: uid,
gecos: gecos.to_string(),
dir: homedir,
shell: shell.clone(),
};
return Ok(passwd);
}
pub(crate) async fn getpwnam(
&mut self,
account_id: &str,
) -> Result<Response, Box<NTSTATUS>> {
// We first try to fetch the user from the cache, so that we
// get the gecos. Otherwise we can just create a passwd entry
// based on whether the upn exists in Entra ID.
let entry = match self.user_cache.fetch(account_id) {
Some(entry) => entry,
#[cfg(not(test))]
None => {
// Check if the user exists in Entra ID
let exists = match self
.client
.lock()
.await
.check_user_exists(&account_id)
.await
{
Ok(exists) => exists,
Err(e) => {
DBG_WARNING!("{:?}", e);
return Ok(Response::NssAccount(None));
}
};
if exists {
return Ok(Response::NssAccount(Some(
self.create_passwd_from_upn(account_id, "")?,
)));
}
return Ok(Response::NssAccount(None));
}
#[cfg(test)]
None => return Ok(Response::NssAccount(None)),
};
return Ok(Response::NssAccount(Some(
self.create_passwd_from_upn(&entry.upn, &entry.name)?,
)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::UserEntry;
use crate::{GroupCache, PrivateCache, UidCache, UserCache};
use idmap::Idmap;
use param::LoadParm;
use tempfile::tempdir;
#[tokio::test]
async fn test_getpwnam() {
// Create a temporary directory for the cache
let dir = tempdir().unwrap();
// Initialize the caches
let private_cache_path = dir
.path()
.join("himmelblau.tdb")
.to_str()
.unwrap()
.to_string();
let pcache = PrivateCache::new(&private_cache_path).unwrap();
let user_cache_path = dir
.path()
.join("himmelblau_users.tdb")
.to_str()
.unwrap()
.to_string();
let mut user_cache = UserCache::new(&user_cache_path).unwrap();
let uid_cache_path = dir
.path()
.join("uid_cache.tdb")
.to_str()
.unwrap()
.to_string();
let uid_cache = UidCache::new(&uid_cache_path).unwrap();
let group_cache_path = dir
.path()
.join("himmelblau_groups.tdb")
.to_str()
.unwrap()
.to_string();
let group_cache = GroupCache::new(&group_cache_path).unwrap();
// Insert a dummy UserEntry into the cache
let dummy_user = UserEntry {
upn: "user1@test.com".to_string(),
uuid: "731e9af3-668d-4033-afd1-9f09b9120cc7".to_string(),
name: "User One".to_string(),
};
let _ = user_cache.store(dummy_user.clone());
// Initialize the Idmap with dummy configuration
let realm = "test.com";
let tenant_id = "89a61bb7-d1b9-4356-a1e0-75d88e06f14e";
let mut idmap = Idmap::new().unwrap();
idmap
.add_gen_domain(realm, tenant_id, (1000, 2000))
.unwrap();
// Initialize dummy configuration for LoadParm
let lp = LoadParm::new(None).expect("Failed loading default config");
// Initialize the Resolver
let mut resolver = Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
};
// Test the getpwnam function with a user that exists in the cache
let result = resolver.getpwnam(&dummy_user.upn).await.unwrap();
match result {
Response::NssAccount(Some(account)) => {
assert_eq!(account.name, dummy_user.upn);
assert_eq!(account.uid, 1316);
assert_eq!(account.gid, 1316);
assert_eq!(account.gecos, dummy_user.name);
assert_eq!(account.dir, "/home/test.com/user1");
assert_eq!(account.shell, "/bin/false");
}
other => {
panic!("Expected NssAccount with Some(account): {:?}", other)
}
}
// Test the getpwnam function with a user that does not exist in the cache
let nonexistent_user_upn = "nonexistent@test.com";
let result = resolver.getpwnam(nonexistent_user_upn).await.unwrap();
match result {
Response::NssAccount(None) => {} // This is the expected result
other => panic!("Expected NssAccount with None: {:?}", other),
}
}
}

View File

@ -0,0 +1,156 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for nss getpwuid
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::himmelblaud::Resolver;
use libc::uid_t;
use ntstatus_gen::NTSTATUS;
use sock::Response;
impl Resolver {
pub(crate) async fn getpwuid(
&mut self,
uid: uid_t,
) -> Result<Response, Box<NTSTATUS>> {
if let Some(upn) = self.uid_cache.fetch(uid) {
if let Some(entry) = self.user_cache.fetch(&upn) {
Ok(Response::NssAccount(Some(
self.create_passwd_from_upn(&entry.upn, &entry.name)?,
)))
} else {
Ok(Response::NssAccount(Some(
self.create_passwd_from_upn(&upn, "")?,
)))
}
} else {
Ok(Response::NssAccount(None))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::UserEntry;
use crate::{GroupCache, PrivateCache, UidCache, UserCache};
use idmap::Idmap;
use param::LoadParm;
use tempfile::tempdir;
#[tokio::test]
async fn test_getpwuid() {
// Create a temporary directory for the cache
let dir = tempdir().unwrap();
// Initialize the caches
let private_cache_path = dir
.path()
.join("himmelblau.tdb")
.to_str()
.unwrap()
.to_string();
let pcache = PrivateCache::new(&private_cache_path).unwrap();
let user_cache_path = dir
.path()
.join("himmelblau_users.tdb")
.to_str()
.unwrap()
.to_string();
let mut user_cache = UserCache::new(&user_cache_path).unwrap();
let uid_cache_path = dir
.path()
.join("uid_cache.tdb")
.to_str()
.unwrap()
.to_string();
let mut uid_cache = UidCache::new(&uid_cache_path).unwrap();
let group_cache_path = dir
.path()
.join("himmelblau_groups.tdb")
.to_str()
.unwrap()
.to_string();
let group_cache = GroupCache::new(&group_cache_path).unwrap();
// Insert a dummy UserEntry into the cache
let dummy_user = UserEntry {
upn: "user1@test.com".to_string(),
uuid: "731e9af3-668d-4033-afd1-9f09b9120cc7".to_string(),
name: "User One".to_string(),
};
let _ = user_cache.store(dummy_user.clone());
// Initialize the Idmap with dummy configuration
let realm = "test.com";
let tenant_id = "89a61bb7-d1b9-4356-a1e0-75d88e06f14e";
let mut idmap = Idmap::new().unwrap();
idmap
.add_gen_domain(realm, tenant_id, (1000, 2000))
.unwrap();
let uid = idmap
.gen_to_unix(tenant_id, &dummy_user.upn)
.expect("Failed to generate uid for user");
// Store the calculated uid -> upn map in the cache
uid_cache
.store(uid, &dummy_user.upn)
.expect("Failed storing generated uid in the cache");
// Initialize dummy configuration for LoadParm
let lp = LoadParm::new(None).expect("Failed loading default config");
// Initialize the Resolver
let mut resolver = Resolver {
realm: realm.to_string(),
tenant_id: tenant_id.to_string(),
lp,
idmap,
pcache,
user_cache,
uid_cache,
group_cache,
};
// Test the getpwuid function with a uid that exists in the cache
let result = resolver.getpwuid(uid).await.unwrap();
match result {
Response::NssAccount(Some(account)) => {
assert_eq!(account.name, dummy_user.upn);
assert_eq!(account.uid, uid);
assert_eq!(account.gid, uid);
assert_eq!(account.gecos, dummy_user.name);
assert_eq!(account.dir, "/home/test.com/user1");
assert_eq!(account.shell, "/bin/false");
}
other => {
panic!("Expected NssAccount with Some(account): {:?}", other)
}
}
// Test the getpwuid function with a uid that does not exist in the cache
let nonexistent_uid = 9999;
let result = resolver.getpwuid(nonexistent_uid).await.unwrap();
match result {
Response::NssAccount(None) => {} // This is the expected result
other => panic!("Expected NssAccount with None: {:?}", other),
}
}
}

View File

@ -0,0 +1,47 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for pam_acct_mgmt
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::himmelblaud::Resolver;
use dbg::DBG_WARNING;
use ntstatus_gen::*;
use sock::Response;
impl Resolver {
pub(crate) async fn pam_acct_mgmt(
&self,
account_id: &str,
) -> Result<Response, Box<NTSTATUS>> {
// Check if the user exists in Entra ID
// TODO: If we're offline, check the cache instead
match self
.client
.lock()
.await
.check_user_exists(&account_id)
.await
{
Ok(exists) => Ok(Response::PamStatus(Some(exists))),
Err(e) => {
DBG_WARNING!("{:?}", e);
Ok(Response::PamStatus(None))
}
}
}
}

View File

@ -0,0 +1,794 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon implementation for PAM_AUTH
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::{GroupEntry, UserEntry};
use crate::himmelblaud::Resolver;
use dbg::{DBG_DEBUG, DBG_ERR, DBG_INFO, DBG_WARNING};
use himmelblau::error::{
MsalError, AUTH_PENDING, DEVICE_AUTH_FAIL, REQUIRES_MFA,
};
use himmelblau::{
DeviceAuthorizationResponse, EnrollAttrs, MFAAuthContinue,
UserToken as UnixUserToken,
};
use ntstatus_gen::*;
use serde::{Deserialize, Serialize};
use sock::{PamAuthRequest, PamAuthResponse, Response};
use std::env;
use std::thread::sleep;
use std::time::Duration;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GroupToken {
pub name: String,
pub spn: String,
pub object_id: String,
pub gidnumber: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserToken {
pub name: String,
pub spn: String,
pub object_id: String,
pub gidnumber: u32,
pub displayname: String,
pub shell: Option<String>,
pub groups: Vec<GroupToken>,
}
pub(crate) enum AuthCredHandler {
MFA { flow: MFAAuthContinue },
DeviceAuthorizationGrant { flow: DeviceAuthorizationResponse },
SetupPin { token: UnixUserToken },
None,
}
pub(crate) enum AuthSession {
InProgress {
account_id: String,
cred_handler: AuthCredHandler,
},
Success,
Denied,
}
impl Resolver {
pub(crate) fn pam_auth_init(
&mut self,
account_id: &str,
) -> Result<(AuthSession, Response), Box<NTSTATUS>> {
let auth_session = AuthSession::InProgress {
account_id: account_id.to_string(),
cred_handler: AuthCredHandler::None,
};
let hello_key = self.pcache.loadable_hello_key_fetch(account_id);
// Skip Hello authentication if it is disabled by config
let hello_enabled =
self.lp.himmelblaud_hello_enabled().map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if !self.is_domain_joined() || hello_key.is_none() || !hello_enabled {
// Send a password request to the client
Ok((
auth_session,
Response::PamAuthStepResponse(PamAuthResponse::Password),
))
} else {
// Send a pin request to the client
Ok((
auth_session,
Response::PamAuthStepResponse(PamAuthResponse::Pin),
))
}
}
pub(crate) async fn pam_auth_step(
&mut self,
account_id: &str,
cred_handler: &mut AuthCredHandler,
pam_next_req: PamAuthRequest,
) -> Result<Response, Box<NTSTATUS>> {
macro_rules! enroll_and_obtain_enrolled_token {
($token:ident) => {{
if !self.is_domain_joined() {
DBG_DEBUG!("Device is not enrolled. Enrolling now.");
self.join_domain(&$token)
.await
.map_err(|e| {
DBG_ERR!("Failed to join domain: {:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
}
let mut tpm = self.hsm.lock().await;
let mtoken2 = self
.client
.lock()
.await
.acquire_token_by_refresh_token(
&$token.refresh_token,
vec!["User.Read"],
Some("https://graph.microsoft.com".to_string()),
&mut tpm,
&self.machine_key,
)
.await;
match mtoken2 {
Ok(token) => token,
Err(e) => {
DBG_ERR!("{:?}", e);
match e {
MsalError::AcquireTokenFailed(err_resp) => {
if err_resp.error_codes.contains(&DEVICE_AUTH_FAIL) {
/* A device authentication failure may happen
* if Azure hasn't finished replicating the new
* device object. Wait 5 seconds and try again. */
DBG_INFO!("Azure hasn't finished replicating the device...");
DBG_INFO!("Retrying in 5 seconds");
sleep(Duration::from_secs(5));
self.client
.lock()
.await
.acquire_token_by_refresh_token(
&$token.refresh_token,
vec!["User.Read"],
Some("https://graph.microsoft.com".to_string()),
&mut tpm,
&self.machine_key,
)
.await
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_FOUND)
})?
} else {
return Err(Box::new(NT_STATUS_NOT_FOUND));
}
}
_ => return Err(Box::new(NT_STATUS_NOT_FOUND)),
}
}
}
}};
}
macro_rules! auth_and_validate_hello_key {
($hello_key:ident, $cred:ident) => {{
let token = {
let mut tpm = self.hsm.lock().await;
self
.client
.lock()
.await
.acquire_token_by_hello_for_business_key(
account_id,
&$hello_key,
vec!["User.Read"],
Some("https://graph.microsoft.com".to_string()),
&mut tpm,
&self.machine_key,
&$cred,
)
.await
.map_err(|e| {
DBG_ERR!(
"Failed to authenticate with hello key: {:?}",
e
);
Box::new(NT_STATUS_LOGON_FAILURE)
})?
};
self.token_validate(account_id, &token).await
}};
}
match (&mut *cred_handler, pam_next_req) {
(
AuthCredHandler::SetupPin { token },
PamAuthRequest::SetupPin { pin },
) => {
let hello_key = {
let mut tpm = self.hsm.lock().await;
match self
.client
.lock()
.await
.provision_hello_for_business_key(
&token,
&mut tpm,
&self.machine_key,
&pin,
)
.await
{
Ok(hello_key) => hello_key,
Err(e) => {
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::SetupPin {
msg: format!(
"Failed to provision hello key: {:?}\n{}",
e,
"Create a PIN to use in place of passwords."
),
}
));
}
}
};
self.pcache
.loadable_hello_key_store(account_id, hello_key.clone())
.map_err(|e| {
DBG_ERR!("Failed to provision hello key: {:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
auth_and_validate_hello_key!(hello_key, pin)
}
(_, PamAuthRequest::Pin { pin }) => {
let hello_key = self
.pcache
.loadable_hello_key_fetch(account_id)
.ok_or_else(|| {
DBG_ERR!("Authentication failed. Hello key missing.");
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
auth_and_validate_hello_key!(hello_key, pin)
}
(_, PamAuthRequest::Password { cred }) => {
// Always attempt to force MFA when enrolling the device, otherwise
// the device object will not have the MFA claim. If we are already
// enrolled but creating a new Hello Pin, we follow the same process,
// since only an enrollment token can be exchanged for a PRT (which
// will be needed to enroll the Hello Pin).
let mresp = self
.client
.lock()
.await
.initiate_acquire_token_by_mfa_flow_for_device_enrollment(
account_id, &cred,
)
.await;
// We need to wait to handle the response until after we've released
// the lock on the client, otherwise we will deadlock.
let resp = match mresp {
Ok(resp) => resp,
Err(e) => {
// If SFA is disabled, we need to skip the SFA fallback.
let sfa_enabled = self
.lp
.himmelblaud_sfa_fallback()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
macro_rules! init_dag {
($msg:expr) => {{
DBG_WARNING!(
"SFA auth failed, falling back to DAG: {}",
$msg
);
// We've exhausted alternatives, and must perform a DAG
let resp = self
.client
.lock()
.await
.initiate_device_flow_for_device_enrollment()
.await
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
let msg = match &resp.message {
Some(msg) => msg.to_string(),
None => format!("Using a browser on another \
device, visit:\n{}\nAnd enter the code:\n{}",
resp.verification_uri, resp.user_code),
};
let polling_interval = match resp.interval {
Some(polling_interval) => polling_interval,
None => 5,
};
*cred_handler = AuthCredHandler::DeviceAuthorizationGrant {
flow: resp,
};
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::MFAPoll {
msg,
polling_interval,
}
));
}}
}
let token = if sfa_enabled {
DBG_WARNING!(
"MFA auth failed, falling back to SFA: {:?}",
e
);
// Again, we need to wait to handle the response until after
// we've released the write lock on the client, otherwise we
// will deadlock.
match self
.client
.lock()
.await
.acquire_token_by_username_password_for_device_enrollment(
account_id, &cred,
)
.await
{
Ok(token) => token,
Err(e) => {
DBG_ERR!("{:?}", e);
match e {
MsalError::AcquireTokenFailed(
err_resp,
) => {
if err_resp
.error_codes
.contains(&REQUIRES_MFA)
{
init_dag!(
err_resp.error_description
);
}
return Err(Box::new(
NT_STATUS_LOGON_FAILURE,
));
}
_ => {
return Err(Box::new(
NT_STATUS_LOGON_FAILURE,
))
}
}
}
}
} else {
init_dag!(
"SFA fallback is disabled by configuration"
)
};
let token2 = enroll_and_obtain_enrolled_token!(token);
return self.token_validate(account_id, &token2).await;
}
};
match resp.mfa_method.as_str() {
"PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => {
let msg = resp.msg.clone();
*cred_handler = AuthCredHandler::MFA { flow: resp };
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::MFACode { msg },
));
}
_ => {
let msg = resp.msg.clone();
let polling_interval =
resp.polling_interval.ok_or_else(|| {
DBG_ERR!("Invalid response from the server");
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
*cred_handler = AuthCredHandler::MFA { flow: resp };
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::MFAPoll {
msg,
// pam expects a polling_interval in
// seconds, not milliseconds.
polling_interval: polling_interval / 1000,
},
));
}
}
}
(
AuthCredHandler::DeviceAuthorizationGrant { flow },
PamAuthRequest::MFAPoll { .. },
) => {
let token = match self
.client
.lock()
.await
.acquire_token_by_device_flow(flow.clone())
.await
{
Err(MsalError::AcquireTokenFailed(ref resp)) => {
if resp.error_codes.contains(&AUTH_PENDING) {
DBG_DEBUG!(
"Polling for acquire_token_by_device_flow"
);
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::MFAPollWait,
));
} else {
DBG_ERR!("{}", resp.error_description);
return Err(Box::new(NT_STATUS_LOGON_FAILURE));
}
}
Err(e) => {
DBG_ERR!("{:?}", e);
return Err(Box::new(NT_STATUS_LOGON_FAILURE));
}
Ok(token) => token,
};
let token2 = enroll_and_obtain_enrolled_token!(token);
match self.token_validate(account_id, &token2).await {
Ok(Response::PamAuthStepResponse(
PamAuthResponse::Success,
)) => {
let mfa = token2.amr_mfa().map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_FOUND)
})?;
// If the DAG didn't obtain an MFA amr, and SFA fallback
// is disabled, we need to reject the authentication
// attempt here.
let sfa_enabled = self
.lp
.himmelblaud_sfa_fallback()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if !mfa && !sfa_enabled {
DBG_INFO!(
"A DAG produced an SFA token, yet SFA \
fallback is disabled by configuration"
);
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::Denied,
));
}
// STOP! If the DAG doesn't hold an MFA amr, then we
// need to bail out here and refuse Hello enrollment
// (we can't enroll in Hello with an SFA token).
// Also skip Hello enrollment if it is disabled by config
let hello_enabled = self
.lp
.himmelblaud_hello_enabled()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if !mfa || !hello_enabled {
if !mfa {
DBG_INFO!(
"Skipping Hello enrollment because \
the token doesn't contain an MFA amr"
);
} else if !hello_enabled {
DBG_INFO!(
"Skipping Hello enrollment \
because it is disabled"
);
}
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::Success,
));
}
// Setup Windows Hello
*cred_handler = AuthCredHandler::SetupPin { token };
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::SetupPin {
msg: format!(
"Set up a PIN\n {}{}",
"A Hello PIN is a fast, secure way to sign",
"in to your device, apps, and services."
),
},
));
}
Ok(auth_result) => Ok(auth_result),
Err(e) => Err(e),
}
}
(
AuthCredHandler::MFA { ref mut flow },
PamAuthRequest::MFACode { cred },
) => {
let token = self
.client
.lock()
.await
.acquire_token_by_mfa_flow(
account_id,
Some(&cred),
None,
flow,
)
.await
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_NOT_FOUND)
})?;
let token2 = enroll_and_obtain_enrolled_token!(token);
match self.token_validate(account_id, &token2).await {
Ok(Response::PamAuthStepResponse(
PamAuthResponse::Success,
)) => {
// Skip Hello enrollment if it is disabled by config
let hello_enabled = self
.lp
.himmelblaud_hello_enabled()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if !hello_enabled {
DBG_INFO!("Skipping Hello enrollment because it is disabled");
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::Success,
));
}
// Setup Windows Hello
*cred_handler = AuthCredHandler::SetupPin { token };
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::SetupPin {
msg: format!(
"Set up a PIN\n {}{}",
"A Hello PIN is a fast, secure way to sign",
"in to your device, apps, and services."
),
},
));
}
Ok(auth_result) => Ok(auth_result),
Err(e) => Err(e),
}
}
(
AuthCredHandler::MFA { flow },
PamAuthRequest::MFAPoll { poll_attempt },
) => {
let max_poll_attempts =
flow.max_poll_attempts.ok_or_else(|| {
DBG_ERR!("Invalid response from the server");
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if poll_attempt > max_poll_attempts {
DBG_ERR!("MFA polling timed out");
return Err(Box::new(NT_STATUS_LOGON_FAILURE));
}
let token = match self
.client
.lock()
.await
.acquire_token_by_mfa_flow(
account_id,
None,
Some(poll_attempt),
flow,
)
.await
{
Ok(token) => token,
Err(e) => match e {
MsalError::MFAPollContinue => {
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::MFAPollWait,
));
}
e => {
DBG_ERR!("{:?}", e);
return Err(Box::new(NT_STATUS_NOT_FOUND));
}
},
};
let token2 = enroll_and_obtain_enrolled_token!(token);
match self.token_validate(account_id, &token2).await {
Ok(Response::PamAuthStepResponse(
PamAuthResponse::Success,
)) => {
// Skip Hello enrollment if it is disabled by config
let hello_enabled = self
.lp
.himmelblaud_hello_enabled()
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if !hello_enabled {
DBG_INFO!("Skipping Hello enrollment because it is disabled");
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::Success,
));
}
// Setup Windows Hello
*cred_handler = AuthCredHandler::SetupPin { token };
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::SetupPin {
msg: format!(
"Set up a PIN\n {}{}",
"A Hello PIN is a fast, secure way to sign",
"in to your device, apps, and services."
),
},
));
}
Ok(auth_result) => Ok(auth_result),
Err(e) => Err(e),
}
}
_ => {
DBG_ERR!(
"Unexpected AuthCredHandler and PamAuthRequest pairing"
);
Err(Box::new(NT_STATUS_NOT_IMPLEMENTED))
}
}
}
async fn token_validate(
&mut self,
account_id: &str,
token: &UnixUserToken,
) -> Result<Response, Box<NTSTATUS>> {
match &token.access_token {
Some(access_token) => {
/* MFA can respond with different user than requested.
* Azure resource names are case insensitive.
*/
let spn = token.spn().map_err(|e| {
DBG_ERR!("Failed fetching user spn: {:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
if account_id.to_string().to_lowercase()
!= spn.to_string().to_lowercase()
{
DBG_ERR!(
"Authenticated user {} does not match requested user {}",
spn, account_id
);
return Ok(Response::PamAuthStepResponse(
PamAuthResponse::Denied,
));
}
DBG_INFO!(
"Authentication successful for user '{}'",
account_id
);
// Store the user in the cache
let user_entry: UserEntry = token.try_into().map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
self.user_cache.store(user_entry).map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
// Get the users groups, and store those groups in the cache
let groups: Vec<GroupEntry> = self
.graph
.request_user_groups(access_token)
.await
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?
.into_iter()
.map(|g| GroupEntry::into_with_member(g, account_id))
.collect();
self.group_cache.merge_groups(account_id, groups).map_err(
|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
},
)?;
Ok(Response::PamAuthStepResponse(PamAuthResponse::Success))
}
None => {
DBG_INFO!("Authentication failed for user '{}'", account_id);
Err(Box::new(NT_STATUS_NOT_FOUND))
}
}
}
async fn join_domain(
&mut self,
token: &UnixUserToken,
) -> Result<(), Box<NTSTATUS>> {
/* If not already joined, join the domain now. */
let attrs = EnrollAttrs::new(
self.realm.clone(),
None,
Some(env::consts::OS.to_string()),
None,
None,
)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
let mut tpm = self.hsm.lock().await;
match self
.client
.lock()
.await
.enroll_device(
&token.refresh_token,
attrs,
&mut tpm,
&self.machine_key,
)
.await
{
Ok((
new_loadable_transport_key,
new_loadable_cert_key,
device_id,
)) => {
DBG_INFO!("Joined domain {} ({})", self.realm, device_id);
// Store the new_loadable_cert_key in the keystore
self.pcache
.loadable_cert_key_store(&self.realm, new_loadable_cert_key)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
// Store the new_loadable_transport_key
self.pcache
.loadable_transport_key_store(
&self.realm,
new_loadable_transport_key,
)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
// Store the device_id
self.pcache
.device_id_store(&self.realm, &device_id)
.map_err(|e| {
DBG_ERR!("{:?}", e);
Box::new(NT_STATUS_LOGON_FAILURE)
})?;
Ok(())
}
Err(e) => {
DBG_ERR!("{:?}", e);
Err(Box::new(NT_STATUS_LOGON_FAILURE))
}
}
}
fn is_domain_joined(&mut self) -> bool {
/* If we have access to tpm keys, and the domain device_id is
* configured, we'll assume we are domain joined. */
let device_id = self.pcache.device_id(&self.realm);
if device_id.is_none() {
return false;
}
let transport_key =
self.pcache.loadable_transport_key_fetch(&self.realm);
if transport_key.is_none() {
return false;
}
let cert_key = self.pcache.loadable_cert_key_fetch(&self.realm);
if cert_key.is_none() {
return false;
}
true
}
}

View File

@ -0,0 +1,476 @@
/*
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/>.
*/
// Ignore unused/dead code when running cargo test
#![cfg_attr(test, allow(unused_imports))]
#![cfg_attr(test, allow(dead_code))]
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 libc::umask;
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, UidCache, UserCache};
mod utils;
#[cfg(not(test))]
#[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);
let frame = talloc::talloc_stackframe!();
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);
talloc::TALLOC_FREE!(frame);
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"
);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
// Setup logging, either to the configured logfile, or to stdout, depending
// on what is specified on the command line.
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);
match lp.logfile() {
Ok(Some(logfile)) => debug_set_logfile(&logfile),
_ => {
eprintln!("Failed to determine logfile name");
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
}
}
}
// Determine the unix socket path
let sock_dir_str = match lp.winbindd_socket_directory() {
Ok(Some(sock_dir)) => sock_dir,
_ => {
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
let sock_dir = Path::new(&sock_dir_str);
let mut sock_path_buf = PathBuf::from(sock_dir);
sock_path_buf.push("hb_pipe");
let sock_path = match sock_path_buf.to_str() {
Some(sock_path) => sock_path,
None => {
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
if sock_path_buf.exists() {
DBG_DEBUG!("Cleaning up socket from previous invocations");
if let Err(_) = std::fs::remove_file(sock_path) {
DBG_ERR!("Failed removing socket");
}
}
// 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");
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
let mut private_dir = Path::new(&private_cache_path).to_path_buf();
private_dir.pop();
if !private_dir.exists() {
DBG_ERR!(
"The private directory '{}' does not exist",
private_dir.display()
);
talloc::TALLOC_FREE!(frame);
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
);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
let cache_dir = match lp.cache_directory() {
Ok(Some(cache_dir)) => cache_dir,
_ => {
DBG_ERR!("Failed to determine cache directory");
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
if !Path::new(&cache_dir).exists() {
DBG_ERR!("The cache directory '{}' does not exist", cache_dir);
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
let uid_cache_path = Path::new(&cache_dir)
.join("himmelblau_uid_map.tdb")
.display()
.to_string();
let uid_cache = match UidCache::new(&uid_cache_path) {
Ok(cache) => cache,
Err(e) => {
DBG_ERR!("Failed to open the himmelblau uid cache: {:?}", e);
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
// Check for and create the hsm pin if required.
let hsm_pin_path = match lp.himmelblaud_hsm_pin_path() {
Ok(Some(hsm_pin_path)) => hsm_pin_path,
_ => {
DBG_ERR!("Failed loading hsm pin path.");
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
let mut hsm_pin_dir = Path::new(&hsm_pin_path).to_path_buf();
hsm_pin_dir.pop();
if !hsm_pin_dir.exists() {
DBG_ERR!(
"The hsm pin directory '{}' does not exist",
hsm_pin_dir.display()
);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
let auth_value =
match utils::hsm_pin_fetch_or_create(&hsm_pin_path).await {
Ok(auth_value) => auth_value,
Err(e) => {
DBG_ERR!("{:?}", e);
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
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.");
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
let mut idmap = match Idmap::new() {
Ok(idmap) => idmap,
Err(e) => {
DBG_ERR!("Failed initializing the idmapper: {:?}", e);
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
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);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
let resolver = Arc::new(Mutex::new(himmelblaud::Resolver::new(
&realm,
&tenant_id,
lp,
idmap,
graph,
pcache,
user_cache,
uid_cache,
group_cache,
hsm,
machine_key,
client,
)));
// Set the umask while we open the path for most clients.
let before = unsafe { umask(0) };
// Listen for incoming 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);
talloc::TALLOC_FREE!(frame);
return ExitCode::FAILURE;
}
};
// Undo umask changes.
let _ = unsafe { umask(before) };
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");
}
}
talloc::TALLOC_FREE!(frame);
ExitCode::SUCCESS
}
.await
}

View File

@ -0,0 +1,149 @@
/*
Unix SMB/CIFS implementation.
Himmelblau daemon common utilities
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 dbg::{DBG_ERR, DBG_INFO};
use kanidm_hsm_crypto::AuthValue;
use ntstatus_gen::*;
use std::path::PathBuf;
use std::str::FromStr;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
pub fn split_username(
username: &str,
) -> Result<(String, String), Box<NTSTATUS>> {
let tup: Vec<&str> = username.split('@').collect();
if tup.len() == 2 {
return Ok((tup[0].to_string(), tup[1].to_string()));
}
Err(Box::new(NT_STATUS_INVALID_USER_PRINCIPAL_NAME))
}
pub(crate) async fn hsm_pin_fetch_or_create(
hsm_pin_path: &str,
) -> Result<AuthValue, Box<NTSTATUS>> {
let auth_value = if !PathBuf::from_str(hsm_pin_path)
.map_err(|e| {
DBG_ERR!("Failed to create hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})?
.exists()
{
let auth_value = AuthValue::generate().map_err(|e| {
DBG_ERR!("Failed to create hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})?;
std::fs::write(hsm_pin_path, auth_value.clone()).map_err(|e| {
DBG_ERR!("Failed to write hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})?;
DBG_INFO!("Generated new HSM pin");
auth_value
} else {
let mut file = File::open(hsm_pin_path).await.map_err(|e| {
DBG_ERR!("Failed to read hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})?;
let mut auth_value = vec![];
file.read_to_end(&mut auth_value).await.map_err(|e| {
DBG_ERR!("Failed to read hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})?;
std::str::from_utf8(&auth_value)
.map_err(|e| {
DBG_ERR!("Failed to read hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})?
.to_string()
};
AuthValue::try_from(auth_value.as_bytes()).map_err(|e| {
DBG_ERR!("Invalid hsm pin: {:?}", e);
Box::new(NT_STATUS_UNSUCCESSFUL)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
use tokio::fs;
#[test]
fn test_split_username_success() {
let username = "user@domain.com";
let result = split_username(username);
assert!(result.is_ok());
let (user, domain) = result.unwrap();
assert_eq!(user, "user");
assert_eq!(domain, "domain.com");
}
#[test]
fn test_split_username_failure() {
let username = "invalid_username";
let result = split_username(username);
assert!(result.is_err());
assert_eq!(*result.unwrap_err(), NT_STATUS_INVALID_USER_PRINCIPAL_NAME);
}
#[tokio::test]
async fn test_hsm_pin_fetch_or_create_generate() {
let dir = tempdir().unwrap();
let path = dir.path().join("hsm_pin");
let result = hsm_pin_fetch_or_create(path.to_str().unwrap()).await;
assert!(result.is_ok());
// Verify that the file is created and contains a valid auth value
let saved_pin = fs::read(path).await.expect("Auth value missing");
AuthValue::try_from(saved_pin.as_slice())
.expect("Failed parsing auth value");
}
#[tokio::test]
async fn test_hsm_pin_fetch_or_create_invalid_path() {
let result = hsm_pin_fetch_or_create("invalid_path\0").await;
assert!(result.is_err());
match result {
Err(e) => assert_eq!(*e, NT_STATUS_UNSUCCESSFUL),
Ok(_) => panic!("Expected error but got success"),
}
}
#[tokio::test]
async fn test_hsm_pin_fetch_or_create_invalid_auth_value() {
let dir = tempdir().unwrap();
let path = dir.path().join("hsm_pin");
// Write invalid content to the file
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(b"invalid_auth_value").unwrap();
// Test reading the invalid file
let result = hsm_pin_fetch_or_create(path.to_str().unwrap()).await;
assert!(result.is_err());
match result {
Err(e) => assert_eq!(*e, NT_STATUS_UNSUCCESSFUL),
Ok(_) => panic!("Expected error but got success"),
}
}
}

15
rust/idmap/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "idmap"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[dependencies]
chelps.workspace = true
dbg.workspace = true
libc.workspace = true
[build-dependencies]
cc = "1.0.97"
bindgen = "0.69.4"

33
rust/idmap/build.rs Normal file
View File

@ -0,0 +1,33 @@
use std::env;
use std::path::Path;
use std::path::PathBuf;
fn main() {
cc::Build::new()
.file("src/sss_idmap.c")
.file("src/sss_idmap_conv.c")
.file("src/murmurhash3.c")
.include(Path::new("../../bin/default/include"))
.warnings(false)
.compile("sss_idmap");
let bindings = bindgen::Builder::default()
.blocklist_function("qgcvt")
.blocklist_function("qgcvt_r")
.blocklist_function("qfcvt")
.blocklist_function("qfcvt_r")
.blocklist_function("qecvt")
.blocklist_function("qecvt_r")
.blocklist_function("strtold")
.header("src/sss_idmap.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
println!("cargo:rustc-link-lib=utf8proc");
println!("cargo:rustc-env=LD_LIBRARY_PATH=../../bin/shared/private/");
}

227
rust/idmap/src/lib.rs Normal file
View File

@ -0,0 +1,227 @@
/*
Himmelblaud
ID-mapping library
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 dbg::DBG_ERR;
use std::ffi::CString;
use std::fmt;
use std::ptr;
use std::sync::{Arc, Mutex};
mod ffi {
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
#[derive(PartialEq, Eq)]
pub struct IdmapError(u32);
pub const IDMAP_SUCCESS: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_SUCCESS);
pub const IDMAP_NOT_IMPLEMENTED: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_NOT_IMPLEMENTED);
pub const IDMAP_ERROR: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_ERROR);
pub const IDMAP_OUT_OF_MEMORY: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_OUT_OF_MEMORY);
pub const IDMAP_NO_DOMAIN: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_NO_DOMAIN);
pub const IDMAP_CONTEXT_INVALID: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_CONTEXT_INVALID);
pub const IDMAP_SID_INVALID: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_SID_INVALID);
pub const IDMAP_SID_UNKNOWN: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_SID_UNKNOWN);
pub const IDMAP_NO_RANGE: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_NO_RANGE);
pub const IDMAP_BUILTIN_SID: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_BUILTIN_SID);
pub const IDMAP_OUT_OF_SLICES: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_OUT_OF_SLICES);
pub const IDMAP_COLLISION: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_COLLISION);
pub const IDMAP_EXTERNAL: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_EXTERNAL);
pub const IDMAP_NAME_UNKNOWN: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_NAME_UNKNOWN);
pub const IDMAP_NO_REVERSE: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_NO_REVERSE);
pub const IDMAP_ERR_LAST: IdmapError =
IdmapError(ffi::idmap_error_code_IDMAP_ERR_LAST);
impl fmt::Display for IdmapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IdmapError({:#x})", self.0)
}
}
impl fmt::Debug for IdmapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IdmapError({:#x})", self.0)
}
}
impl std::error::Error for IdmapError {}
pub struct Idmap {
ctx: Arc<Mutex<*mut ffi::sss_idmap_ctx>>,
}
impl Idmap {
pub fn new() -> Result<Idmap, IdmapError> {
let mut ctx = ptr::null_mut();
unsafe {
match IdmapError(ffi::sss_idmap_init(
None,
ptr::null_mut(),
None,
&mut ctx,
)) {
IDMAP_SUCCESS => Ok(Idmap {
ctx: Arc::new(Mutex::new(ctx)),
}),
e => Err(e),
}
}
}
pub fn add_gen_domain(
&mut self,
domain_name: &str,
tenant_id: &str,
range: (u32, u32),
) -> Result<(), IdmapError> {
let ctx = self.ctx.lock().map_err(|e| {
DBG_ERR!("Failed obtaining write lock on sss_idmap_ctx: {}", e);
IDMAP_ERROR
})?;
let domain_name_cstr =
CString::new(domain_name).map_err(|_| IDMAP_OUT_OF_MEMORY)?;
let tenant_id_cstr =
CString::new(tenant_id).map_err(|_| IDMAP_OUT_OF_MEMORY)?;
let mut idmap_range = ffi::sss_idmap_range {
min: range.0,
max: range.1,
};
unsafe {
match IdmapError(ffi::sss_idmap_add_gen_domain_ex(
*ctx,
domain_name_cstr.as_ptr(),
tenant_id_cstr.as_ptr(),
&mut idmap_range,
ptr::null_mut(),
None,
None,
ptr::null_mut(),
0,
false,
)) {
IDMAP_SUCCESS => Ok(()),
e => Err(e),
}
}
}
pub fn gen_to_unix(
&self,
tenant_id: &str,
input: &str,
) -> Result<u32, IdmapError> {
let ctx = self.ctx.lock().map_err(|e| {
DBG_ERR!("Failed obtaining write lock on sss_idmap_ctx: {}", e);
IDMAP_ERROR
})?;
let tenant_id_cstr =
CString::new(tenant_id).map_err(|_| IDMAP_OUT_OF_MEMORY)?;
let input_cstr = CString::new(input.to_lowercase())
.map_err(|_| IDMAP_OUT_OF_MEMORY)?;
unsafe {
let mut id: u32 = 0;
match IdmapError(ffi::sss_idmap_gen_to_unix(
*ctx,
tenant_id_cstr.as_ptr(),
input_cstr.as_ptr(),
&mut id,
)) {
IDMAP_SUCCESS => Ok(id),
e => Err(e),
}
}
}
}
impl Drop for Idmap {
fn drop(&mut self) {
match self.ctx.lock() {
Ok(ctx) => unsafe {
let _ = ffi::sss_idmap_free(*ctx);
},
Err(e) => {
DBG_ERR!(
"Failed obtaining write lock on sss_idmap_ctx during drop: {}",
e
);
}
}
}
}
unsafe impl Send for Idmap {}
unsafe impl Sync for Idmap {}
#[cfg(test)]
mod tests {
use crate::Idmap;
use std::collections::HashMap;
pub const DEFAULT_IDMAP_RANGE: (u32, u32) = (200000, 2000200000);
#[test]
fn sssd_idmapping() {
let domain = "contoso.onmicrosoft.com";
let tenant_id = "d7af6c1b-0497-40fe-9d17-07e6b0f8332e";
let mut idmap = Idmap::new().expect("Idmap initialization failed");
idmap
.add_gen_domain(domain, tenant_id, DEFAULT_IDMAP_RANGE)
.expect("Failed initializing test domain idmapping");
// Verify we always get the same mapping for various users
let mut usermap: HashMap<String, u32> = HashMap::new();
usermap.insert("tux@contoso.onmicrosoft.com".to_string(), 1912749799);
usermap.insert("admin@contoso.onmicrosoft.com".to_string(), 297515919);
usermap.insert("dave@contoso.onmicrosoft.com".to_string(), 132631922);
usermap.insert("joe@contoso.onmicrosoft.com".to_string(), 361591965);
usermap.insert("georg@contoso.onmicrosoft.com".to_string(), 866887005);
for (username, expected_uid) in &usermap {
let uid = idmap.gen_to_unix(tenant_id, username).expect(&format!(
"Failed converting username {} to uid",
username
));
assert_eq!(
uid, *expected_uid,
"Uid for {} did not match",
username
);
}
}
}

View File

@ -0,0 +1,114 @@
/* This file is based on the public domain MurmurHash3 from Austin Appleby:
* http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp
*
* We use only the 32 bit variant because the 2 produce different result while
* we need to produce the same result regardless of the architecture as
* clients can be both 64 or 32 bit at the same time.
*/
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include "config.h"
#include "murmurhash3.h"
#include "util.h"
static uint32_t rotl(uint32_t x, int8_t r)
{
return (x << r) | (x >> (32 - r));
}
/* slower than original but is endian neutral and handles platforms that
* do only aligned reads */
__attribute__((always_inline))
static inline uint32_t getblock(const uint8_t *p, int i)
{
uint32_t r;
size_t size = sizeof(uint32_t);
memcpy(&r, &p[i * size], size);
return le32toh(r);
}
/*
* Finalization mix - force all bits of a hash block to avalanche
*/
__attribute__((always_inline))
static inline uint32_t fmix(uint32_t h)
{
h ^= h >> 16;
h *= 0x85ebca6b;
h ^= h >> 13;
h *= 0xc2b2ae35;
h ^= h >> 16;
return h;
}
uint32_t murmurhash3(const char *key, int len, uint32_t seed)
{
const uint8_t *blocks;
const uint8_t *tail;
int nblocks;
uint32_t h1;
uint32_t k1;
uint32_t c1;
uint32_t c2;
int i;
blocks = (const uint8_t *)key;
nblocks = len / 4;
h1 = seed;
c1 = 0xcc9e2d51;
c2 = 0x1b873593;
/* body */
for (i = 0; i < nblocks; i++) {
k1 = getblock(blocks, i);
k1 *= c1;
k1 = rotl(k1, 15);
k1 *= c2;
h1 ^= k1;
h1 = rotl(h1, 13);
h1 = h1 * 5 + 0xe6546b64;
}
/* tail */
tail = (const uint8_t *)key + nblocks * 4;
k1 = 0;
switch (len & 3) {
case 3:
k1 ^= tail[2] << 16;
SSS_ATTRIBUTE_FALLTHROUGH;
case 2:
k1 ^= tail[1] << 8;
SSS_ATTRIBUTE_FALLTHROUGH;
case 1:
k1 ^= tail[0];
k1 *= c1;
k1 = rotl(k1, 15);
k1 *= c2;
h1 ^= k1;
break;
default:
break;
}
/* finalization */
h1 ^= len;
h1 = fmix(h1);
return h1;
}

View File

@ -0,0 +1,21 @@
/* This file is based on the public domain MurmurHash3 from Austin Appleby:
* http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp
*
* We use only the 32 bit variant because the 2 produce different result while
* we need to produce the same result regardless of the architecture as
* clients can be both 64 or 32 bit at the same time.
*/
#ifndef _SHARED_MURMURHASH3_H_
#define _SHARED_MURMURHASH3_H_
/* CAUTION:
* This file is also used in sss_client (pam, nss). Therefore it have to be
* minimalist and cannot include DEBUG macros or header file util.h.
*/
#include <stdint.h>
uint32_t murmurhash3(const char *key, int len, uint32_t seed);
#endif /* _SHARED_MURMURHASH3_H_ */

1909
rust/idmap/src/sss_idmap.c Normal file

File diff suppressed because it is too large Load Diff

1100
rust/idmap/src/sss_idmap.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,570 @@
/*
SSSD
ID-mapping library - conversion utilities
Authors:
Sumit Bose <sbose@redhat.com>
Copyright (C) 2012 Red Hat
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/>.
*/
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include "sss_idmap.h"
#include "sss_idmap_private.h"
//#include "util/util.h"
//#include "util/sss_endian.h"
#include "util.h"
#define SID_ID_AUTHS 6
#define SID_SUB_AUTHS 15
struct sss_dom_sid {
uint8_t sid_rev_num;
int8_t num_auths; /* [range(0,15)] */
uint8_t id_auth[SID_ID_AUTHS]; /* highest order byte has index 0 */
uint32_t sub_auths[SID_SUB_AUTHS]; /* host byte-order */
};
enum idmap_error_code sss_idmap_bin_sid_to_dom_sid(struct sss_idmap_ctx *ctx,
const uint8_t *bin_sid,
size_t length,
struct sss_dom_sid **_dom_sid)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid;
size_t i = 0;
size_t p = 0;
uint32_t val;
CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
if (length > sizeof(struct sss_dom_sid)) return IDMAP_SID_INVALID;
dom_sid = ctx->alloc_func(sizeof(struct sss_dom_sid), ctx->alloc_pvt);
if (dom_sid == NULL) {
return IDMAP_OUT_OF_MEMORY;
}
memset(dom_sid, 0, sizeof(struct sss_dom_sid));
/* Safely copy in the SID revision number */
dom_sid->sid_rev_num = (uint8_t) *(bin_sid + p);
p++;
/* Safely copy in the number of sub auth values */
dom_sid->num_auths = (uint8_t) *(bin_sid + p);
p++;
/* Make sure we aren't being told to read more bin_sid
* than can fit in the structure
*/
if (dom_sid->num_auths > SID_SUB_AUTHS) {
err = IDMAP_SID_INVALID;
goto done;
}
/* Safely copy in the id_auth values */
for (i = 0; i < SID_ID_AUTHS; i++) {
dom_sid->id_auth[i] = (uint8_t) *(bin_sid + p);
p++;
}
/* Safely copy in the sub_auths values */
for (i = 0; i < dom_sid->num_auths; i++) {
/* SID sub auth values in Active Directory are stored little-endian,
* we store them in host order */
SAFEALIGN_COPY_UINT32(&val, bin_sid + p, &p);
dom_sid->sub_auths[i] = le32toh(val);
}
*_dom_sid = dom_sid;
err = IDMAP_SUCCESS;
done:
if (err != IDMAP_SUCCESS) {
ctx->free_func(dom_sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_dom_sid_to_bin_sid(struct sss_idmap_ctx *ctx,
struct sss_dom_sid *dom_sid,
uint8_t **_bin_sid,
size_t *_length)
{
enum idmap_error_code err;
uint8_t *bin_sid;
size_t length;
size_t i = 0;
size_t p = 0;
uint32_t val;
CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
if (dom_sid->num_auths > SID_SUB_AUTHS) {
return IDMAP_SID_INVALID;
}
length = 2 + SID_ID_AUTHS + dom_sid->num_auths * 4;
bin_sid = ctx->alloc_func(length, ctx->alloc_pvt);
if (bin_sid == NULL) {
return IDMAP_OUT_OF_MEMORY;
}
bin_sid[p] = dom_sid->sid_rev_num;
p++;
bin_sid[p] = dom_sid->num_auths;
p++;
for (i = 0; i < SID_ID_AUTHS; i++) {
bin_sid[p] = dom_sid->id_auth[i];
p++;
}
for (i = 0; i < dom_sid->num_auths; i++) {
if (p + sizeof(uint32_t) > length) {
err = IDMAP_SID_INVALID;
goto done;
}
val = htole32(dom_sid->sub_auths[i]);
SAFEALIGN_COPY_UINT32(bin_sid + p, &val, &p);
}
*_bin_sid = bin_sid;
*_length = length;
err = IDMAP_SUCCESS;
done:
if (err != IDMAP_SUCCESS) {
ctx->free_func(bin_sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_dom_sid_to_sid(struct sss_idmap_ctx *ctx,
struct sss_dom_sid *dom_sid,
char **_sid)
{
enum idmap_error_code err;
char *sid_buf;
size_t sid_buf_len;
char *p;
int nc;
int8_t i;
uint32_t id_auth_val = 0;
if (dom_sid->num_auths > SID_SUB_AUTHS) {
return IDMAP_SID_INVALID;
}
sid_buf_len = 25 + dom_sid->num_auths * 11;
sid_buf = ctx->alloc_func(sid_buf_len, ctx->alloc_pvt);
if (sid_buf == NULL) {
return IDMAP_OUT_OF_MEMORY;
}
memset(sid_buf, 0, sid_buf_len);
/* Only 32bits are used for the string representation */
id_auth_val = (dom_sid->id_auth[2] << 24) +
(dom_sid->id_auth[3] << 16) +
(dom_sid->id_auth[4] << 8) +
(dom_sid->id_auth[5]);
nc = snprintf(sid_buf, sid_buf_len, "S-%u-%lu", dom_sid->sid_rev_num,
(unsigned long) id_auth_val);
if (nc < 0 || nc >= sid_buf_len) {
err = IDMAP_SID_INVALID;
goto done;
}
/* Loop through the sub-auths, if any, prepending a hyphen
* for each one.
*/
p = sid_buf;
for (i = 0; i < dom_sid->num_auths ; i++) {
p += nc;
sid_buf_len -= nc;
nc = snprintf(p, sid_buf_len, "-%lu",
(unsigned long) dom_sid->sub_auths[i]);
if (nc < 0 || nc >= sid_buf_len) {
err = IDMAP_SID_INVALID;
goto done;
}
}
*_sid = sid_buf;
err = IDMAP_SUCCESS;
done:
if (err != IDMAP_SUCCESS) {
ctx->free_func(sid_buf, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_sid_to_dom_sid(struct sss_idmap_ctx *ctx,
const char *sid,
struct sss_dom_sid **_dom_sid)
{
enum idmap_error_code err;
unsigned long ul;
char *r;
char *end;
struct sss_dom_sid *dom_sid;
CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
if (sid == NULL || (sid[0] != 'S' && sid[0] != 's') || sid[1] != '-') {
return IDMAP_SID_INVALID;
}
dom_sid = ctx->alloc_func(sizeof(struct sss_dom_sid), ctx->alloc_pvt);
if (dom_sid == NULL) {
return IDMAP_OUT_OF_MEMORY;
}
memset(dom_sid, 0, sizeof(struct sss_dom_sid));
if (!isdigit(sid[2])) {
err = IDMAP_SID_INVALID;
goto done;
}
errno = 0;
ul = strtoul(sid + 2, &r, 10);
if (errno != 0 || r == NULL || *r != '-' || ul > UINT8_MAX) {
err = IDMAP_SID_INVALID;
goto done;
}
dom_sid->sid_rev_num = (uint8_t) ul;
r++;
if (!isdigit(*r)) {
err = IDMAP_SID_INVALID;
goto done;
}
errno = 0;
ul = strtoul(r, &r, 10);
if (errno != 0 || r == NULL || ul > UINT32_MAX) {
err = IDMAP_SID_INVALID;
goto done;
}
/* id_auth in the string should always be <2^32 in decimal */
/* store values in the same order as the binary representation */
dom_sid->id_auth[0] = 0;
dom_sid->id_auth[1] = 0;
dom_sid->id_auth[2] = (ul & 0xff000000) >> 24;
dom_sid->id_auth[3] = (ul & 0x00ff0000) >> 16;
dom_sid->id_auth[4] = (ul & 0x0000ff00) >> 8;
dom_sid->id_auth[5] = (ul & 0x000000ff);
if (*r == '\0') {
/* no sub auths given */
err = IDMAP_SUCCESS;
goto done;
}
if (*r != '-') {
err = IDMAP_SID_INVALID;
goto done;
}
do {
if (dom_sid->num_auths >= SID_SUB_AUTHS) {
err = IDMAP_SID_INVALID;
goto done;
}
r++;
if (!isdigit(*r)) {
err = IDMAP_SID_INVALID;
goto done;
}
errno = 0;
ul = strtoul(r, &end, 10);
if (errno != 0 || ul > UINT32_MAX || end == NULL ||
(*end != '\0' && *end != '-')) {
err = IDMAP_SID_INVALID;
goto done;
}
dom_sid->sub_auths[dom_sid->num_auths++] = ul;
r = end;
} while (*r != '\0');
err = IDMAP_SUCCESS;
done:
if (err != IDMAP_SUCCESS) {
ctx->free_func(dom_sid, ctx->alloc_pvt);
} else {
*_dom_sid = dom_sid;
}
return err;
}
enum idmap_error_code sss_idmap_sid_to_bin_sid(struct sss_idmap_ctx *ctx,
const char *sid,
uint8_t **_bin_sid,
size_t *_length)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid = NULL;
size_t length;
uint8_t *bin_sid = NULL;
err = sss_idmap_sid_to_dom_sid(ctx, sid, &dom_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
err = sss_idmap_dom_sid_to_bin_sid(ctx, dom_sid, &bin_sid, &length);
if (err != IDMAP_SUCCESS) {
goto done;
}
*_length = length;
*_bin_sid = bin_sid;
err = IDMAP_SUCCESS;
done:
ctx->free_func(dom_sid, ctx->alloc_pvt);
if (err != IDMAP_SUCCESS) {
ctx->free_func(bin_sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_bin_sid_to_sid(struct sss_idmap_ctx *ctx,
const uint8_t *bin_sid,
size_t length,
char **_sid)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid = NULL;
char *sid = NULL;
err = sss_idmap_bin_sid_to_dom_sid(ctx, bin_sid, length, &dom_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
err = sss_idmap_dom_sid_to_sid(ctx, dom_sid, &sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
*_sid = sid;
err = IDMAP_SUCCESS;
done:
ctx->free_func(dom_sid, ctx->alloc_pvt);
if (err != IDMAP_SUCCESS) {
ctx->free_func(sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_sid_to_smb_sid(struct sss_idmap_ctx *ctx,
const char *sid,
struct dom_sid **_smb_sid)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid = NULL;
struct dom_sid *smb_sid = NULL;
err = sss_idmap_sid_to_dom_sid(ctx, sid, &dom_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
err = sss_idmap_dom_sid_to_smb_sid(ctx, dom_sid, &smb_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
*_smb_sid = smb_sid;
err = IDMAP_SUCCESS;
done:
ctx->free_func(dom_sid, ctx->alloc_pvt);
if (err != IDMAP_SUCCESS) {
ctx->free_func(smb_sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_smb_sid_to_sid(struct sss_idmap_ctx *ctx,
struct dom_sid *smb_sid,
char **_sid)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid = NULL;
char *sid = NULL;
err = sss_idmap_smb_sid_to_dom_sid(ctx, smb_sid, &dom_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
err = sss_idmap_dom_sid_to_sid(ctx, dom_sid, &sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
*_sid = sid;
err = IDMAP_SUCCESS;
done:
ctx->free_func(dom_sid, ctx->alloc_pvt);
if (err != IDMAP_SUCCESS) {
ctx->free_func(sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_dom_sid_to_smb_sid(struct sss_idmap_ctx *ctx,
struct sss_dom_sid *dom_sid,
struct dom_sid **_smb_sid)
{
struct dom_sid *smb_sid;
size_t c;
smb_sid = ctx->alloc_func(sizeof(struct dom_sid), ctx->alloc_pvt);
if (smb_sid == NULL) {
return IDMAP_OUT_OF_MEMORY;
}
memset(smb_sid, 0, sizeof(struct dom_sid));
smb_sid->sid_rev_num = dom_sid->sid_rev_num;
smb_sid->num_auths = dom_sid->num_auths;
for (c = 0; c < SID_ID_AUTHS; c++) {
smb_sid->id_auth[c] = dom_sid->id_auth[c];
}
for (c = 0; c < SID_SUB_AUTHS; c++) {
smb_sid->sub_auths[c] = dom_sid->sub_auths[c];
}
*_smb_sid = smb_sid;
return IDMAP_SUCCESS;
}
enum idmap_error_code sss_idmap_smb_sid_to_dom_sid(struct sss_idmap_ctx *ctx,
struct dom_sid *smb_sid,
struct sss_dom_sid **_dom_sid)
{
struct sss_dom_sid *dom_sid;
size_t c;
dom_sid = ctx->alloc_func(sizeof(struct sss_dom_sid), ctx->alloc_pvt);
if (dom_sid == NULL) {
return IDMAP_OUT_OF_MEMORY;
}
memset(dom_sid, 0, sizeof(struct sss_dom_sid));
dom_sid->sid_rev_num = smb_sid->sid_rev_num;
dom_sid->num_auths = smb_sid->num_auths;
for (c = 0; c < SID_ID_AUTHS; c++) {
dom_sid->id_auth[c] = smb_sid->id_auth[c];
}
for (c = 0; c < SID_SUB_AUTHS; c++) {
dom_sid->sub_auths[c] = smb_sid->sub_auths[c];
}
*_dom_sid = dom_sid;
return IDMAP_SUCCESS;
}
enum idmap_error_code sss_idmap_bin_sid_to_smb_sid(struct sss_idmap_ctx *ctx,
const uint8_t *bin_sid,
size_t length,
struct dom_sid **_smb_sid)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid = NULL;
struct dom_sid *smb_sid = NULL;
err = sss_idmap_bin_sid_to_dom_sid(ctx, bin_sid, length, &dom_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
err = sss_idmap_dom_sid_to_smb_sid(ctx, dom_sid, &smb_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
*_smb_sid = smb_sid;
err = IDMAP_SUCCESS;
done:
ctx->free_func(dom_sid, ctx->alloc_pvt);
if (err != IDMAP_SUCCESS) {
ctx->free_func(smb_sid, ctx->alloc_pvt);
}
return err;
}
enum idmap_error_code sss_idmap_smb_sid_to_bin_sid(struct sss_idmap_ctx *ctx,
struct dom_sid *smb_sid,
uint8_t **_bin_sid,
size_t *_length)
{
enum idmap_error_code err;
struct sss_dom_sid *dom_sid = NULL;
uint8_t *bin_sid = NULL;
size_t length;
err = sss_idmap_smb_sid_to_dom_sid(ctx, smb_sid, &dom_sid);
if (err != IDMAP_SUCCESS) {
goto done;
}
err = sss_idmap_dom_sid_to_bin_sid(ctx, dom_sid, &bin_sid, &length);
if (err != IDMAP_SUCCESS) {
goto done;
}
*_bin_sid = bin_sid;
*_length = length;
err = IDMAP_SUCCESS;
done:
ctx->free_func(dom_sid, ctx->alloc_pvt);
if (err != IDMAP_SUCCESS) {
ctx->free_func(bin_sid, ctx->alloc_pvt);
}
return err;
}

View File

@ -0,0 +1,84 @@
/*
SSSD
ID-mapping library - private headers
Authors:
Sumit Bose <sbose@redhat.com>
Copyright (C) 2012 Red Hat
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/>.
*/
#ifndef SSS_IDMAP_PRIVATE_H_
#define SSS_IDMAP_PRIVATE_H_
#define SSS_IDMAP_DEFAULT_LOWER 200000
#define SSS_IDMAP_DEFAULT_UPPER 2000200000
#define SSS_IDMAP_DEFAULT_RANGESIZE 200000
#define SSS_IDMAP_DEFAULT_AUTORID false
#define SSS_IDMAP_DEFAULT_EXTRA_SLICE_INIT 10
#define CHECK_IDMAP_CTX(ctx, ret) do { \
if (ctx == NULL || ctx->alloc_func == NULL || ctx->free_func == NULL) { \
return ret; \
} \
} while(0)
struct sss_idmap_opts {
/* true if autorid compatibility mode is used */
bool autorid_mode;
/* smallest available id (for all domains) */
id_t idmap_lower;
/* highest available id (for all domains) */
id_t idmap_upper;
/* number of available UIDs (for single domain) */
id_t rangesize;
/* maximal number of secondary slices */
int extra_slice_init;
};
struct sss_idmap_ctx {
idmap_alloc_func *alloc_func;
void *alloc_pvt;
idmap_free_func *free_func;
struct sss_idmap_opts idmap_opts;
struct idmap_domain_info *idmap_domain_info;
};
/* This is a copy of the definition in the samba gen_ndr/security.h header
* file. We use it here to be able to offer conversions form struct dom_sid to
* string or binary representation since those are not made available by
* public samba libraries.
*
* If the definition ever changes on the samba side we have to adopt the
* change. But chances are very low that this will ever happen since e.g. this
* struct is also defined in public documentation from Microsoft. See e.g.
* section 2.4.2.3 of "[MS-DTYP]: Windows Data Types"
* http://msdn.microsoft.com/en-us/library/cc230364(v=prot.10)
*/
struct dom_sid {
uint8_t sid_rev_num;
int8_t num_auths;
uint8_t id_auth[6];
uint32_t sub_auths[15];
};
#endif /* SSS_IDMAP_PRIVATE_H_ */

46
rust/idmap/src/util.h Normal file
View File

@ -0,0 +1,46 @@
/*
Himmelblaud
ID-mapping library utils
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/>.
*/
#ifndef _UTILS_H
#define _UTILS_H
#include <string.h>
#include <stdint.h>
static inline void
safealign_memcpy(void *dest, const void *src, size_t n, size_t *counter)
{
memcpy(dest, src, n);
if (counter) {
*counter += n;
}
}
#define SAFEALIGN_COPY_UINT32(dest, src, pctr) \
safealign_memcpy(dest, src, sizeof(uint32_t), pctr)
#if defined(__GNUC__) && __GNUC__ >= 7
#define SSS_ATTRIBUTE_FALLTHROUGH __attribute__ ((fallthrough))
#else
#define SSS_ATTRIBUTE_FALLTHROUGH
#endif
#endif /* _UTILS_H */

21
rust/nss/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "nss_himmelblau"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[lib]
name = "nss_himmelblau"
crate-type = [ "cdylib" ]
path = "src/lib.rs"
[dependencies]
libc.workspace = true
libnss = "0.8.0"
ntstatus_gen.workspace = true
param = { workspace = true }
sock = { workspace = true }
[build-dependencies]
version = { path = "../version" }

18
rust/nss/build.rs Normal file
View File

@ -0,0 +1,18 @@
fn main() {
if let Some(vers) = version::samba_version_string() {
println!("cargo:rustc-env=CARGO_PKG_VERSION={}", vers);
}
println!(
"cargo:rustc-env=CARGO_PKG_VERSION_MAJOR={}",
version::SAMBA_VERSION_MAJOR
);
println!(
"cargo:rustc-env=CARGO_PKG_VERSION_MINOR={}",
version::SAMBA_VERSION_MINOR
);
println!(
"cargo:rustc-env=CARGO_PKG_VERSION_PATCH={}",
version::SAMBA_VERSION_RELEASE
);
println!("cargo:rustc-env=LD_LIBRARY_PATH=../../bin/shared:../../bin/shared/private/");
}

185
rust/nss/src/lib.rs Normal file
View File

@ -0,0 +1,185 @@
/*
Unix SMB/CIFS implementation.
NSS module 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/>.
*/
#[macro_use]
extern crate libnss;
use libnss::group::{Group as NssGroup, GroupHooks};
use libnss::interop::Response as NssResponse;
use libnss::passwd::{Passwd as NssPasswd, PasswdHooks};
use param::LoadParm;
use sock::{stream_and_timeout, Group, Passwd, Request, Response};
struct HimmelblauPasswd;
libnss_passwd_hooks!(himmelblau, HimmelblauPasswd);
impl PasswdHooks for HimmelblauPasswd {
fn get_all_entries() -> NssResponse<Vec<NssPasswd>> {
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return NssResponse::Unavail,
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok((stream, timeout)) => (stream, timeout),
Err(_) => return NssResponse::Unavail,
};
let req = Request::NssAccounts;
stream
.send(&req, timeout)
.map(|r| match r {
Response::NssAccounts(l) => {
l.into_iter().map(|passwd| passwd.into()).collect()
}
_ => vec![],
})
.map(NssResponse::Success)
.unwrap_or_else(|_| NssResponse::Success(vec![]))
}
fn get_entry_by_uid(uid: libc::uid_t) -> NssResponse<NssPasswd> {
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return NssResponse::Unavail,
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok((stream, timeout)) => (stream, timeout),
Err(_) => return NssResponse::Unavail,
};
let req = Request::NssAccountByUid(uid);
stream
.send(&req, timeout)
.map(|r| match r {
Response::NssAccount(passwd) => passwd
.map(nsspasswd_from_passwd)
.map(NssResponse::Success)
.unwrap_or_else(|| NssResponse::NotFound),
_ => NssResponse::NotFound,
})
.unwrap_or_else(|_| NssResponse::NotFound)
}
fn get_entry_by_name(name: String) -> NssResponse<NssPasswd> {
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return NssResponse::Unavail,
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok((stream, timeout)) => (stream, timeout),
Err(_) => return NssResponse::Unavail,
};
let req = Request::NssAccountByName(name);
stream
.send(&req, timeout)
.map(|r| match r {
Response::NssAccount(passwd) => passwd
.map(nsspasswd_from_passwd)
.map(NssResponse::Success)
.unwrap_or_else(|| NssResponse::NotFound),
_ => NssResponse::NotFound,
})
.unwrap_or_else(|_| NssResponse::NotFound)
}
}
struct HimmelblauGroup;
libnss_group_hooks!(himmelblau, HimmelblauGroup);
impl GroupHooks for HimmelblauGroup {
fn get_all_entries() -> NssResponse<Vec<NssGroup>> {
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return NssResponse::Unavail,
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok((stream, timeout)) => (stream, timeout),
Err(_) => return NssResponse::Unavail,
};
let req = Request::NssGroups;
stream
.send(&req, timeout)
.map(|r| match r {
Response::NssGroups(l) => {
l.into_iter().map(|group| group.into()).collect()
}
_ => vec![],
})
.map(NssResponse::Success)
.unwrap_or_else(|_| NssResponse::Success(vec![]))
}
fn get_entry_by_gid(gid: libc::gid_t) -> NssResponse<NssGroup> {
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return NssResponse::Unavail,
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok((stream, timeout)) => (stream, timeout),
Err(_) => return NssResponse::Unavail,
};
let req = Request::NssGroupByGid(gid);
stream
.send(&req, timeout)
.map(|r| match r {
Response::NssGroup(group) => group
.map(nssgroup_from_group)
.map(NssResponse::Success)
.unwrap_or_else(|| NssResponse::NotFound),
_ => NssResponse::NotFound,
})
.unwrap_or_else(|_| NssResponse::NotFound)
}
fn get_entry_by_name(name: String) -> NssResponse<NssGroup> {
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return NssResponse::Unavail,
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok((stream, timeout)) => (stream, timeout),
Err(_) => return NssResponse::Unavail,
};
let req = Request::NssGroupByName(name);
stream
.send(&req, timeout)
.map(|r| match r {
Response::NssGroup(group) => group
.map(nssgroup_from_group)
.map(NssResponse::Success)
.unwrap_or_else(|| NssResponse::NotFound),
_ => NssResponse::NotFound,
})
.unwrap_or_else(|_| NssResponse::NotFound)
}
}
fn nsspasswd_from_passwd(passwd: Passwd) -> NssPasswd {
passwd.into()
}
fn nssgroup_from_group(group: Group) -> NssGroup {
group.into()
}

View File

@ -0,0 +1,6 @@
[package]
name = "ntstatus_gen"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true

View File

@ -0,0 +1,24 @@
/*
Unix SMB/CIFS implementation.
Generated NTSTATUS Errors
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/>.
*/
#[path = "../../../bin/default/libcli/util/ntstatus_gen.rs"]
mod ntstatus_gen;
pub use ntstatus_gen::*;

23
rust/pam/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "pam_himmelblau"
links = "pam"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[lib]
name = "pam_himmelblau"
crate-type = [ "cdylib" ]
path = "src/lib.rs"
[dependencies]
libc = { workspace = true }
dbg = { workspace = true }
sock.workspace = true
chelps.workspace = true
param.workspace = true
[build-dependencies]
pkg-config = "0.3.30"

10
rust/pam/build.rs Normal file
View File

@ -0,0 +1,10 @@
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// ignore errors here since older versions of pam do not ship the pkg-config `pam.pc` file.
// Not setting anything here will fall back on just blindly linking with `-lpam`,
// which will work on environments with libpam.so, but no pkg-config file.
let _ = pkg_config::Config::new()
.atleast_version("1.3.0")
.probe("pam");
println!("cargo:rustc-env=LD_LIBRARY_PATH=../../bin/shared:../../bin/shared/private/");
}

43
rust/pam/src/lib.rs Normal file
View File

@ -0,0 +1,43 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#![deny(warnings)]
#![warn(unused_extern_crates)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
// In this file, we do want to panic on these faults.
// #![deny(clippy::unwrap_used)]
// #![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
#[cfg(target_family = "unix")]
mod pam;
// pub use needs to be here so it'll compile and export all the things
#[cfg(target_family = "unix")]
pub use crate::pam::*;

View File

@ -0,0 +1,118 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
use libc::{c_int, c_uint};
pub type PamFlag = c_uint;
pub type PamItemType = c_int;
pub type PamMessageStyle = c_int;
pub type AlwaysZero = c_int;
// The Linux-PAM flags
// see /usr/include/security/_pam_types.h
pub const _PAM_SILENT: PamFlag = 0x8000;
pub const _PAM_DISALLOW_NULL_AUTHTOK: PamFlag = 0x0001;
pub const _PAM_ESTABLISH_CRED: PamFlag = 0x0002;
pub const _PAM_DELETE_CRED: PamFlag = 0x0004;
pub const _PAM_REINITIALIZE_CRED: PamFlag = 0x0008;
pub const _PAM_REFRESH_CRED: PamFlag = 0x0010;
pub const _PAM_CHANGE_EXPIRED_AUTHTOK: PamFlag = 0x0020;
// The Linux-PAM item types
// see /usr/include/security/_pam_types.h
/// The service name
pub const PAM_SERVICE: PamItemType = 1;
/// The user name
pub const PAM_USER: PamItemType = 2;
/// The tty name
pub const PAM_TTY: PamItemType = 3;
/// The remote host name
pub const PAM_RHOST: PamItemType = 4;
/// The pam_conv structure
pub const PAM_CONV: PamItemType = 5;
/// The authentication token (password)
pub const PAM_AUTHTOK: PamItemType = 6;
/// The old authentication token
pub const PAM_OLDAUTHTOK: PamItemType = 7;
/// The remote user name
pub const PAM_RUSER: PamItemType = 8;
/// the prompt for getting a username
pub const PAM_USER_PROMPT: PamItemType = 9;
/* Linux-PAM :extensionsPamItemType = */
/// app supplied function to override failure delays
pub const _PAM_FAIL_DELAY: PamItemType = 10;
/// X :display name
pub const _PAM_XDISPLAY: PamItemType = 11;
/// X :server authentication data
pub const _PAM_XAUTHDATA: PamItemType = 12;
/// The type for pam_get_authtok
pub const _PAM_AUTHTOK_TYPE: PamItemType = 13;
// Message styles
pub const PAM_PROMPT_ECHO_OFF: PamMessageStyle = 1;
pub const PAM_PROMPT_ECHO_ON: PamMessageStyle = 2;
pub const PAM_ERROR_MSG: PamMessageStyle = 3;
pub const PAM_TEXT_INFO: PamMessageStyle = 4;
/// yes/no/maybe conditionals
pub const _PAM_RADIO_TYPE: PamMessageStyle = 5;
pub const _PAM_BINARY_PROMPT: PamMessageStyle = 7;
// The Linux-PAM return values
// see /usr/include/security/_pam_types.h
#[allow(non_camel_case_types, dead_code)]
#[derive(Debug, PartialEq)]
#[repr(C)]
pub enum PamResultCode {
PAM_SUCCESS = 0,
PAM_OPEN_ERR = 1,
PAM_SYMBOL_ERR = 2,
PAM_SERVICE_ERR = 3,
PAM_SYSTEM_ERR = 4,
PAM_BUF_ERR = 5,
PAM_PERM_DENIED = 6,
PAM_AUTH_ERR = 7,
PAM_CRED_INSUFFICIENT = 8,
PAM_AUTHINFO_UNAVAIL = 9,
PAM_USER_UNKNOWN = 10,
PAM_MAXTRIES = 11,
PAM_NEW_AUTHTOK_REQD = 12,
PAM_ACCT_EXPIRED = 13,
PAM_SESSION_ERR = 14,
PAM_CRED_UNAVAIL = 15,
PAM_CRED_EXPIRED = 16,
PAM_CRED_ERR = 17,
PAM_NO_MODULE_DATA = 18,
PAM_CONV_ERR = 19,
PAM_AUTHTOK_ERR = 20,
PAM_AUTHTOK_RECOVERY_ERR = 21,
PAM_AUTHTOK_LOCK_BUSY = 22,
PAM_AUTHTOK_DISABLE_AGING = 23,
PAM_TRY_AGAIN = 24,
PAM_IGNORE = 25,
PAM_ABORT = 26,
PAM_AUTHTOK_EXPIRED = 27,
PAM_MODULE_UNKNOWN = 28,
PAM_BAD_ITEM = 29,
PAM_CONV_AGAIN = 30,
PAM_INCOMPLETE = 31,
}

112
rust/pam/src/pam/conv.rs Normal file
View File

@ -0,0 +1,112 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
use std::ffi::{CStr, CString};
use std::ptr;
use libc::{c_char, c_int};
use crate::pam::constants::{PamResultCode, *};
use crate::pam::module::{PamItem, PamResult};
#[allow(missing_copy_implementations)]
pub enum AppDataPtr {}
#[repr(C)]
struct PamMessage {
msg_style: PamMessageStyle,
msg: *const c_char,
}
#[repr(C)]
struct PamResponse {
resp: *const c_char,
resp_retcode: AlwaysZero,
}
/// `PamConv` acts as a channel for communicating with user.
///
/// Communication is mediated by the pam client (the application that invoked
/// pam). Messages sent will be relayed to the user by the client, and response
/// will be relayed back.
#[repr(C)]
pub struct PamConv {
conv: extern "C" fn(
num_msg: c_int,
pam_message: &&PamMessage,
pam_response: &mut *const PamResponse,
appdata_ptr: *const AppDataPtr,
) -> PamResultCode,
appdata_ptr: *const AppDataPtr,
}
impl PamConv {
/// Sends a message to the pam client.
///
/// This will typically result in the user seeing a message or a prompt.
/// There are several message styles available:
///
/// - PAM_PROMPT_ECHO_OFF
/// - PAM_PROMPT_ECHO_ON
/// - PAM_ERROR_MSG
/// - PAM_TEXT_INFO
/// - PAM_RADIO_TYPE
/// - PAM_BINARY_PROMPT
///
/// Note that the user experience will depend on how the client implements
/// these message styles - and not all applications implement all message
/// styles.
pub fn send(
&self,
style: PamMessageStyle,
msg: &str,
) -> PamResult<Option<String>> {
let mut resp_ptr: *const PamResponse = ptr::null();
let msg_cstr = CString::new(msg).unwrap();
let msg = PamMessage {
msg_style: style,
msg: msg_cstr.as_ptr(),
};
let ret = (self.conv)(1, &&msg, &mut resp_ptr, self.appdata_ptr);
if PamResultCode::PAM_SUCCESS == ret {
// PamResponse.resp is null for styles that don't return user input like PAM_TEXT_INFO
let response = unsafe { (*resp_ptr).resp };
if response.is_null() {
Ok(None)
} else {
let bytes = unsafe { CStr::from_ptr(response).to_bytes() };
Ok(String::from_utf8(bytes.to_vec()).ok())
}
} else {
Err(ret)
}
}
}
impl PamItem for PamConv {
fn item_type() -> PamItemType {
PAM_CONV
}
}

101
rust/pam/src/pam/items.rs Normal file
View File

@ -0,0 +1,101 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
use crate::pam::constants::{
PamItemType, PAM_AUTHTOK, PAM_OLDAUTHTOK, PAM_RHOST, PAM_RUSER,
PAM_SERVICE, PAM_TTY, PAM_USER, PAM_USER_PROMPT,
};
pub use crate::pam::conv::PamConv;
use crate::pam::module::PamItem;
#[allow(dead_code)]
pub struct PamService {}
impl PamItem for PamService {
fn item_type() -> PamItemType {
PAM_SERVICE
}
}
#[allow(dead_code)]
pub struct PamUser {}
impl PamItem for PamUser {
fn item_type() -> PamItemType {
PAM_USER
}
}
#[allow(dead_code)]
pub struct PamUserPrompt {}
impl PamItem for PamUserPrompt {
fn item_type() -> PamItemType {
PAM_USER_PROMPT
}
}
#[allow(dead_code)]
pub struct PamTty {}
impl PamItem for PamTty {
fn item_type() -> PamItemType {
PAM_TTY
}
}
#[allow(dead_code)]
pub struct PamRUser {}
impl PamItem for PamRUser {
fn item_type() -> PamItemType {
PAM_RUSER
}
}
#[allow(dead_code)]
pub struct PamRHost {}
impl PamItem for PamRHost {
fn item_type() -> PamItemType {
PAM_RHOST
}
}
#[allow(dead_code)]
pub struct PamAuthTok {}
impl PamItem for PamAuthTok {
fn item_type() -> PamItemType {
PAM_AUTHTOK
}
}
#[allow(dead_code)]
pub struct PamOldAuthTok {}
impl PamItem for PamOldAuthTok {
fn item_type() -> PamItemType {
PAM_OLDAUTHTOK
}
}

140
rust/pam/src/pam/macros.rs Normal file
View File

@ -0,0 +1,140 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/// Macro to generate the `extern "C"` entrypoint bindings needed by PAM
///
/// You can call `pam_hooks!(SomeType);` for any type that implements `PamHooks`
///
/// ## Examples:
///
/// Here is full example of a PAM module that would authenticate and authorize everybody:
///
/// ```
/// #[macro_use]
/// extern crate pam;
///
/// use pam::constants::{PamFlag, PamResultCode};
/// use pam::module::{PamHandle, PamHooks};
/// use std::ffi::CStr;
///
/// # fn main() {}
/// struct MyPamModule;
/// pam_hooks!(MyPamModule);
///
/// impl PamHooks for MyPamModule {
/// fn sm_authenticate(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
/// println!("Everybody is authenticated!");
/// PamResultCode::PAM_SUCCESS
/// }
///
/// fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
/// println!("Everybody is authorized!");
/// PamResultCode::PAM_SUCCESS
/// }
/// }
/// ```
#[macro_export]
macro_rules! pam_hooks {
($ident:ident) => {
pub use self::pam_hooks_scope::*;
mod pam_hooks_scope {
use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
use $crate::pam::constants::{PamFlag, PamResultCode};
use $crate::pam::module::{PamHandle, PamHooks};
fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
(0..argc)
.map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
.collect()
}
#[no_mangle]
pub extern "C" fn pam_sm_acct_mgmt(
pamh: &PamHandle,
flags: PamFlag,
argc: c_int,
argv: *const *const c_char,
) -> PamResultCode {
let args = extract_argv(argc, argv);
super::$ident::acct_mgmt(pamh, args, flags)
}
#[no_mangle]
pub extern "C" fn pam_sm_authenticate(
pamh: &PamHandle,
flags: PamFlag,
argc: c_int,
argv: *const *const c_char,
) -> PamResultCode {
let args = extract_argv(argc, argv);
super::$ident::sm_authenticate(pamh, args, flags)
}
#[no_mangle]
pub extern "C" fn pam_sm_chauthtok(
pamh: &PamHandle,
flags: PamFlag,
argc: c_int,
argv: *const *const c_char,
) -> PamResultCode {
let args = extract_argv(argc, argv);
super::$ident::sm_chauthtok(pamh, args, flags)
}
#[no_mangle]
pub extern "C" fn pam_sm_close_session(
pamh: &PamHandle,
flags: PamFlag,
argc: c_int,
argv: *const *const c_char,
) -> PamResultCode {
let args = extract_argv(argc, argv);
super::$ident::sm_close_session(pamh, args, flags)
}
#[no_mangle]
pub extern "C" fn pam_sm_open_session(
pamh: &PamHandle,
flags: PamFlag,
argc: c_int,
argv: *const *const c_char,
) -> PamResultCode {
let args = extract_argv(argc, argv);
super::$ident::sm_open_session(pamh, args, flags)
}
#[no_mangle]
pub extern "C" fn pam_sm_setcred(
pamh: &PamHandle,
flags: PamFlag,
argc: c_int,
argv: *const *const c_char,
) -> PamResultCode {
let args = extract_argv(argc, argv);
super::$ident::sm_setcred(pamh, args, flags)
}
}
};
}

615
rust/pam/src/pam/mod.rs Executable file
View File

@ -0,0 +1,615 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Copyright (c) 2020 William Brown <william@blackhats.net.au>
Copyright (c) 2024 David Mulder <dmulder@samba.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
//! Interface to the pluggable authentication module framework (PAM).
//!
//! The goal of this library is to provide a type-safe API that can be used to
//! interact with PAM. The library is incomplete - currently it supports
//! a subset of functions for use in a pam authentication module. A pam module
//! is a shared library that is invoked to authenticate a user, or to perform
//! other functions.
//!
//! For general information on writing pam modules, see
//! [The Linux-PAM Module Writers' Guide][module-guide]
//!
//! [module-guide]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_MWG.html
//!
//! A typical authentication module will define an external function called
//! `pam_sm_authenticate()`, which will use functions in this library to
//! interrogate the program that requested authentication for more information,
//! and to render a result. For a working example that uses this library, see
//! [toznyauth-pam][].
//!
//! [toznyauth-pam]: https://github.com/tozny/toznyauth-pam
//!
//! Note that constants that are normally read from pam header files are
//! hard-coded in the `constants` module. The values there are taken from
//! a Linux system. That means that it might take some work to get this library
//! to work on other platforms.
pub mod constants;
pub mod conv;
pub mod items;
#[doc(hidden)]
pub mod macros;
pub mod module;
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::ffi::CStr;
use dbg::*;
use param::LoadParm;
use sock::{
stream_and_timeout, PamAuthRequest, PamAuthResponse, Request, Response,
};
use crate::pam::constants::*;
use crate::pam::conv::PamConv;
use crate::pam::module::{PamHandle, PamHooks};
use crate::pam_hooks;
use constants::PamResultCode;
use std::thread;
use std::time::Duration;
fn install_subscriber(lp: &LoadParm, debug: bool) {
debuglevel_set!(if debug { DBGLVL_DEBUG } else { DBGLVL_ERR });
match lp.logfile() {
Ok(Some(logfile)) => {
debug_set_logfile(&logfile);
setup_logging(env!("CARGO_PKG_NAME"), DEBUG_FILE)
}
_ => setup_logging(env!("CARGO_PKG_NAME"), DEBUG_STDOUT),
}
}
#[derive(Debug)]
struct Options {
debug: bool,
use_first_pass: bool,
ignore_unknown_user: bool,
}
impl TryFrom<&Vec<&CStr>> for Options {
type Error = ();
fn try_from(args: &Vec<&CStr>) -> Result<Self, Self::Error> {
let opts: Result<BTreeSet<&str>, _> =
args.iter().map(|cs| cs.to_str()).collect();
let gopts = match opts {
Ok(o) => o,
Err(e) => {
println!("Error in module args -> {:?}", e);
return Err(());
}
};
Ok(Options {
debug: gopts.contains("debug"),
use_first_pass: gopts.contains("use_first_pass"),
ignore_unknown_user: gopts.contains("ignore_unknown_user"),
})
}
}
pub struct PamHimmelblau;
pam_hooks!(PamHimmelblau);
macro_rules! match_sm_auth_client_response {
($expr:expr, $opts:ident, $conv:ident, $req:ident, $authtok:ident, $($pat:pat => $result:expr),*) => {
match $expr {
Ok(r) => match r {
$($pat => $result),*
Response::PamAuthStepResponse(PamAuthResponse::Success) => {
return PamResultCode::PAM_SUCCESS;
}
Response::PamAuthStepResponse(PamAuthResponse::Denied) => {
return PamResultCode::PAM_AUTH_ERR;
}
Response::PamAuthStepResponse(PamAuthResponse::Unknown) => {
if $opts.ignore_unknown_user {
return PamResultCode::PAM_IGNORE;
} else {
return PamResultCode::PAM_USER_UNKNOWN;
}
}
Response::PamAuthStepResponse(PamAuthResponse::SetupPin {
msg,
}) => {
match $conv.send(PAM_TEXT_INFO, &msg) {
Ok(_) => {}
Err(err) => {
if $opts.debug {
println!("Message prompt failed");
}
return err;
}
}
let mut pin;
let mut confirm;
loop {
pin = match $conv.send(PAM_PROMPT_ECHO_OFF, "New PIN: ") {
Ok(password) => match password {
Some(cred) => cred,
None => {
DBG_DEBUG!("no pin");
return PamResultCode::PAM_CRED_INSUFFICIENT;
}
},
Err(err) => {
DBG_DEBUG!("unable to get pin");
return err;
}
};
confirm = match $conv.send(PAM_PROMPT_ECHO_OFF, "Confirm PIN: ") {
Ok(password) => match password {
Some(cred) => cred,
None => {
DBG_DEBUG!("no confirmation pin");
return PamResultCode::PAM_CRED_INSUFFICIENT;
}
},
Err(err) => {
DBG_DEBUG!("unable to get confirmation pin");
return err;
}
};
if pin == confirm {
break;
} else {
match $conv.send(PAM_TEXT_INFO, "Inputs did not match. Try again.") {
Ok(_) => {}
Err(err) => {
if $opts.debug {
println!("Message prompt failed");
}
return err;
}
}
}
}
// Now setup the request for the next loop.
$req = Request::PamAuthenticateStep(PamAuthRequest::SetupPin {
pin,
});
continue;
},
Response::PamAuthStepResponse(PamAuthResponse::Pin) => {
let mut consume_authtok = None;
// Swap the authtok out with a None, so it can only be consumed once.
// If it's already been swapped, we are just swapping two null pointers
// here effectively.
std::mem::swap(&mut $authtok, &mut consume_authtok);
let cred = if let Some(cred) = consume_authtok {
cred
} else {
match $conv.send(PAM_PROMPT_ECHO_OFF, "PIN: ") {
Ok(password) => match password {
Some(cred) => cred,
None => {
DBG_DEBUG!("no pin");
return PamResultCode::PAM_CRED_INSUFFICIENT;
}
},
Err(err) => {
DBG_DEBUG!("unable to get pin");
return err;
}
}
};
// Now setup the request for the next loop.
$req = Request::PamAuthenticateStep(PamAuthRequest::Pin { pin: cred });
continue;
}
_ => {
// unexpected response.
DBG_ERR!("PAM_IGNORE, unexpected resolver response: {:?}", r);
return PamResultCode::PAM_IGNORE;
}
},
Err(e) => {
DBG_ERR!("PAM_IGNORE: {:?}", e);
return PamResultCode::PAM_IGNORE;
}
}
}
}
impl PamHooks for PamHimmelblau {
fn acct_mgmt(
pamh: &PamHandle,
args: Vec<&CStr>,
_flags: PamFlag,
) -> PamResultCode {
let opts = match Options::try_from(&args) {
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
install_subscriber(&lp, opts.debug);
let tty = pamh.get_tty();
let rhost = pamh.get_rhost();
DBG_DEBUG!("{:?} {:?} {:?} {:?} acct_mgmt", args, opts, tty, rhost);
let account_id = match pamh.get_user(None) {
Ok(aid) => aid,
Err(e) => {
DBG_ERR!("get_user: {:?}", e);
return e;
}
};
let req = Request::PamAccountAllowed(account_id);
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok(res) => res,
Err(e) => {
DBG_ERR!("Error stream_and_timeout: {:?}", e);
return PamResultCode::PAM_SERVICE_ERR;
}
};
match stream.send(&req, timeout) {
Ok(r) => match r {
Response::PamStatus(Some(true)) => {
DBG_DEBUG!("PamResultCode::PAM_SUCCESS");
PamResultCode::PAM_SUCCESS
}
Response::PamStatus(Some(false)) => {
DBG_DEBUG!("PamResultCode::PAM_AUTH_ERR");
PamResultCode::PAM_AUTH_ERR
}
Response::PamStatus(None) => {
if opts.ignore_unknown_user {
DBG_DEBUG!("PamResultCode::PAM_IGNORE");
PamResultCode::PAM_IGNORE
} else {
DBG_DEBUG!("PamResultCode::PAM_USER_UNKNOWN");
PamResultCode::PAM_USER_UNKNOWN
}
}
_ => {
// unexpected response.
DBG_ERR!(
"PAM_IGNORE, unexpected resolver response: {:?}",
r
);
PamResultCode::PAM_IGNORE
}
},
Err(e) => {
DBG_ERR!("PamResultCode::PAM_IGNORE: {:?}", e);
PamResultCode::PAM_IGNORE
}
}
}
fn sm_authenticate(
pamh: &PamHandle,
args: Vec<&CStr>,
_flags: PamFlag,
) -> PamResultCode {
let opts = match Options::try_from(&args) {
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
install_subscriber(&lp, opts.debug);
let tty = pamh.get_tty();
let rhost = pamh.get_rhost();
DBG_DEBUG!(
"{:?} {:?} {:?} {:?} sm_authenticate",
args,
opts,
tty,
rhost
);
let account_id = match pamh.get_user(None) {
Ok(aid) => aid,
Err(e) => {
DBG_ERR!("get_user: {:?}", e);
return e;
}
};
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok(res) => res,
Err(e) => {
DBG_ERR!("Error stream_and_timeout: {:?}", e);
return PamResultCode::PAM_SERVICE_ERR;
}
};
// Later we may need to move this to a function and call it as a oneshot for auth methods
// that don't require any authtoks at all. For example, imagine a user authed and they
// needed to follow a URL to continue. In that case, they would fail here because they
// didn't enter an authtok that they didn't need!
let mut authtok = match pamh.get_authtok() {
Ok(Some(v)) => Some(v),
Ok(None) => {
if opts.use_first_pass {
DBG_DEBUG!("Don't have an authtok, returning PAM_AUTH_ERR");
return PamResultCode::PAM_AUTH_ERR;
}
None
}
Err(e) => {
DBG_ERR!("get_authtok: {:?}", e);
return e;
}
};
let conv = match pamh.get_item::<PamConv>() {
Ok(conv) => conv,
Err(e) => {
DBG_ERR!("pam_conv: {:?}", e);
return e;
}
};
let mut req = Request::PamAuthenticateInit(account_id);
loop {
match_sm_auth_client_response!(stream.send(&req, timeout), opts, conv, req, authtok,
Response::PamAuthStepResponse(PamAuthResponse::Password) => {
let mut consume_authtok = None;
// Swap the authtok out with a None, so it can only be consumed once.
// If it's already been swapped, we are just swapping two null pointers
// here effectively.
std::mem::swap(&mut authtok, &mut consume_authtok);
let cred = if let Some(cred) = consume_authtok {
cred
} else {
match conv.send(PAM_PROMPT_ECHO_OFF, "Password: ") {
Ok(password) => match password {
Some(cred) => cred,
None => {
DBG_DEBUG!("no password");
return PamResultCode::PAM_CRED_INSUFFICIENT;
}
},
Err(err) => {
DBG_DEBUG!("unable to get password");
return err;
}
}
};
// Now setup the request for the next loop.
req = Request::PamAuthenticateStep(PamAuthRequest::Password { cred });
continue;
},
Response::PamAuthStepResponse(PamAuthResponse::MFACode {
msg,
}) => {
match conv.send(PAM_TEXT_INFO, &msg) {
Ok(_) => {}
Err(err) => {
if opts.debug {
println!("Message prompt failed");
}
return err;
}
}
let cred = match conv.send(PAM_PROMPT_ECHO_OFF, "Code: ") {
Ok(password) => match password {
Some(cred) => cred,
None => {
DBG_DEBUG!("no mfa code");
return PamResultCode::PAM_CRED_INSUFFICIENT;
}
},
Err(err) => {
DBG_DEBUG!("unable to get mfa code");
return err;
}
};
// Now setup the request for the next loop.
req = Request::PamAuthenticateStep(PamAuthRequest::MFACode {
cred,
});
continue;
},
Response::PamAuthStepResponse(PamAuthResponse::MFAPoll {
msg,
polling_interval,
}) => {
match conv.send(PAM_TEXT_INFO, &msg) {
Ok(_) => {}
Err(err) => {
if opts.debug {
println!("Message prompt failed");
}
return err;
}
}
// Necessary because of OpenSSH bug
// https://bugzilla.mindrot.org/show_bug.cgi?id=2876 -
// PAM_TEXT_INFO and PAM_ERROR_MSG conversation not
// honoured during PAM authentication
let _ = conv.send(PAM_PROMPT_ECHO_OFF, "Press enter to continue");
let mut poll_attempt = 0;
req = Request::PamAuthenticateStep(
PamAuthRequest::MFAPoll { poll_attempt }
);
loop {
thread::sleep(Duration::from_secs(polling_interval.into()));
match_sm_auth_client_response!(
stream.send(&req, timeout), opts, conv, req, authtok,
Response::PamAuthStepResponse(
PamAuthResponse::MFAPollWait,
) => {
// Continue polling if the daemon says to wait
poll_attempt += 1;
req = Request::PamAuthenticateStep(
PamAuthRequest::MFAPoll { poll_attempt }
);
continue;
}
);
}
}
);
} // while true, continue calling PamAuthenticateStep until we get a decision.
}
fn sm_chauthtok(
_pamh: &PamHandle,
args: Vec<&CStr>,
_flags: PamFlag,
) -> PamResultCode {
let opts = match Options::try_from(&args) {
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
install_subscriber(&lp, opts.debug);
DBG_DEBUG!("{:?} {:?} sm_chauthtok", args, opts);
PamResultCode::PAM_IGNORE
}
fn sm_close_session(
_pamh: &PamHandle,
args: Vec<&CStr>,
_flags: PamFlag,
) -> PamResultCode {
let opts = match Options::try_from(&args) {
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
install_subscriber(&lp, opts.debug);
DBG_DEBUG!("{:?} {:?} sm_close_session", args, opts);
PamResultCode::PAM_SUCCESS
}
fn sm_open_session(
pamh: &PamHandle,
args: Vec<&CStr>,
_flags: PamFlag,
) -> PamResultCode {
let opts = match Options::try_from(&args) {
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
install_subscriber(&lp, opts.debug);
DBG_DEBUG!("{:?} {:?} sm_open_session", args, opts);
let account_id = match pamh.get_user(None) {
Ok(aid) => aid,
Err(e) => {
DBG_ERR!("get_user: {:?}", e);
return e;
}
};
let req = Request::PamAccountBeginSession(account_id);
let (mut stream, timeout) = match stream_and_timeout(&lp) {
Ok(res) => res,
Err(e) => {
DBG_ERR!("Error stream_and_timeout: {:?}", e);
return PamResultCode::PAM_SERVICE_ERR;
}
};
match stream.send(&req, timeout) {
Ok(Response::Success) => PamResultCode::PAM_SUCCESS,
other => {
DBG_DEBUG!("PAM_IGNORE: {:?}", other);
PamResultCode::PAM_IGNORE
}
}
}
fn sm_setcred(
_pamh: &PamHandle,
args: Vec<&CStr>,
_flags: PamFlag,
) -> PamResultCode {
let opts = match Options::try_from(&args) {
Ok(o) => o,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
let lp = match LoadParm::new(None) {
Ok(lp) => lp,
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
};
install_subscriber(&lp, opts.debug);
DBG_DEBUG!("{:?} {:?} sm_setcred", args, opts);
PamResultCode::PAM_SUCCESS
}
}

361
rust/pam/src/pam/module.rs Executable file
View File

@ -0,0 +1,361 @@
/*
MIT License
Copyright (c) 2015 TOZNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
//! Functions for use in pam modules.
use std::ffi::{CStr, CString};
use std::{mem, ptr};
use libc::c_char;
use crate::pam::constants::{
PamFlag, PamItemType, PamResultCode, PAM_AUTHTOK, PAM_RHOST, PAM_TTY,
};
/// Opaque type, used as a pointer when making pam API calls.
///
/// A module is invoked via an external function such as `pam_sm_authenticate`.
/// Such a call provides a pam handle pointer. The same pointer should be given
/// as an argument when making API calls.
#[allow(missing_copy_implementations)]
pub enum PamHandle {}
#[allow(missing_copy_implementations)]
enum PamItemT {}
#[allow(missing_copy_implementations)]
pub enum PamDataT {}
#[link(name = "pam")]
extern "C" {
fn pam_get_data(
pamh: *const PamHandle,
module_data_name: *const c_char,
data: &mut *const PamDataT,
) -> PamResultCode;
fn pam_set_data(
pamh: *const PamHandle,
module_data_name: *const c_char,
data: *mut PamDataT,
cleanup: unsafe extern "C" fn(
pamh: *const PamHandle,
data: *mut PamDataT,
error_status: PamResultCode,
),
) -> PamResultCode;
fn pam_get_item(
pamh: *const PamHandle,
item_type: PamItemType,
item: &mut *const PamItemT,
) -> PamResultCode;
fn pam_set_item(
pamh: *mut PamHandle,
item_type: PamItemType,
item: &PamItemT,
) -> PamResultCode;
fn pam_get_user(
pamh: *const PamHandle,
user: &*mut c_char,
prompt: *const c_char,
) -> PamResultCode;
}
/// # Safety
///
/// We're doing what we can for this one, but it's FFI.
pub unsafe extern "C" fn cleanup<T>(
_: *const PamHandle,
c_data: *mut PamDataT,
_: PamResultCode,
) {
let c_data = Box::from_raw(c_data);
let data: Box<T> = mem::transmute(c_data);
mem::drop(data);
}
pub type PamResult<T> = Result<T, PamResultCode>;
/// # Safety
///
/// Type-level mapping for safely retrieving values with `get_item`.
///
/// See `pam_get_item` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub trait PamItem {
/// Maps a Rust type to a pam constant.
///
/// For example, the type PamConv maps to the constant PAM_CONV. The pam
/// API contract specifies that when the API function `pam_get_item` is
/// called with the constant PAM_CONV, it will return a value of type
/// `PamConv`.
fn item_type() -> PamItemType;
}
impl PamHandle {
/// # Safety
///
/// Gets some value, identified by `key`, that has been set by the module
/// previously.
///
/// See `pam_get_data` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub unsafe fn get_data<'a, T>(&'a self, key: &str) -> PamResult<&'a T> {
let c_key = CString::new(key).unwrap();
let mut ptr: *const PamDataT = ptr::null();
let res = pam_get_data(self, c_key.as_ptr(), &mut ptr);
if PamResultCode::PAM_SUCCESS == res && !ptr.is_null() {
let typed_ptr: *const T = ptr as *const T;
let data: &T = &*typed_ptr;
Ok(data)
} else {
Err(res)
}
}
/// Stores a value that can be retrieved later with `get_data`. The value lives
/// as long as the current pam cycle.
///
/// See `pam_set_data` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn set_data<T>(&self, key: &str, data: Box<T>) -> PamResult<()> {
let c_key = CString::new(key).unwrap();
let res = unsafe {
let c_data: Box<PamDataT> = mem::transmute(data);
let c_data = Box::into_raw(c_data);
pam_set_data(self, c_key.as_ptr(), c_data, cleanup::<T>)
};
if PamResultCode::PAM_SUCCESS == res {
Ok(())
} else {
Err(res)
}
}
/// Retrieves a value that has been set, possibly by the pam client. This is
/// particularly useful for getting a `PamConv` reference.
///
/// See `pam_get_item` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn get_item<'a, T: PamItem>(&self) -> PamResult<&'a T> {
let mut ptr: *const PamItemT = ptr::null();
let (res, item) = unsafe {
let r = pam_get_item(self, T::item_type(), &mut ptr);
let typed_ptr: *const T = ptr as *const T;
let t: &T = &*typed_ptr;
(r, t)
};
if PamResultCode::PAM_SUCCESS == res {
Ok(item)
} else {
Err(res)
}
}
/// Sets a value in the pam context. The value can be retrieved using
/// `get_item`.
///
/// Note that all items are strings, except `PAM_CONV` and `PAM_FAIL_DELAY`.
///
/// See `pam_set_item` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn set_item_str<T: PamItem>(&mut self, item: &str) -> PamResult<()> {
let c_item = CString::new(item).unwrap();
let res = unsafe {
pam_set_item(
self,
T::item_type(),
// unwrapping is okay here, as c_item will not be a NULL
// pointer
(c_item.as_ptr() as *const PamItemT).as_ref().unwrap(),
)
};
if PamResultCode::PAM_SUCCESS == res {
Ok(())
} else {
Err(res)
}
}
/// Retrieves the name of the user who is authenticating or logging in.
///
/// This is really a specialization of `get_item`.
///
/// See `pam_get_user` in
/// <http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html>
pub fn get_user(&self, prompt: Option<&str>) -> PamResult<String> {
let ptr: *mut c_char = ptr::null_mut();
let res = match prompt {
Some(p) => {
let c_prompt = CString::new(p).unwrap();
unsafe { pam_get_user(self, &ptr, c_prompt.as_ptr()) }
}
None => unsafe { pam_get_user(self, &ptr, ptr::null()) },
};
if PamResultCode::PAM_SUCCESS == res && !ptr.is_null() {
let const_ptr = ptr as *const c_char;
let bytes = unsafe { CStr::from_ptr(const_ptr).to_bytes() };
String::from_utf8(bytes.to_vec())
.map_err(|_| PamResultCode::PAM_CONV_ERR)
} else {
Err(res)
}
}
pub fn get_authtok(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null();
let (res, item) = unsafe {
let r = pam_get_item(self, PAM_AUTHTOK, &mut ptr);
let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() {
let typed_ptr: *const c_char = ptr as *const c_char;
Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned())
} else {
None
};
(r, t)
};
if PamResultCode::PAM_SUCCESS == res {
Ok(item)
} else {
Err(res)
}
}
pub fn get_tty(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null();
let (res, item) = unsafe {
let r = pam_get_item(self, PAM_TTY, &mut ptr);
let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() {
let typed_ptr: *const c_char = ptr as *const c_char;
Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned())
} else {
None
};
(r, t)
};
if PamResultCode::PAM_SUCCESS == res {
Ok(item)
} else {
Err(res)
}
}
pub fn get_rhost(&self) -> PamResult<Option<String>> {
let mut ptr: *const PamItemT = ptr::null();
let (res, item) = unsafe {
let r = pam_get_item(self, PAM_RHOST, &mut ptr);
let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() {
let typed_ptr: *const c_char = ptr as *const c_char;
Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned())
} else {
None
};
(r, t)
};
if PamResultCode::PAM_SUCCESS == res {
Ok(item)
} else {
Err(res)
}
}
}
/// Provides functions that are invoked by the entrypoints generated by the
/// [`pam_hooks!` macro](../macro.pam_hooks.html).
///
/// All of hooks are ignored by PAM dispatch by default given the default return value of `PAM_IGNORE`.
/// Override any functions that you want to handle with your module. See `man pam(3)`.
#[allow(unused_variables)]
pub trait PamHooks {
/// This function performs the task of establishing whether the user is permitted to gain access at
/// this time. It should be understood that the user has previously been validated by an
/// authentication module. This function checks for other things. Such things might be: the time of
/// day or the date, the terminal line, remote hostname, etc. This function may also determine
/// things like the expiration on passwords, and respond that the user change it before continuing.
fn acct_mgmt(
pamh: &PamHandle,
args: Vec<&CStr>,
flags: PamFlag,
) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
/// This function performs the task of authenticating the user.
fn sm_authenticate(
pamh: &PamHandle,
args: Vec<&CStr>,
flags: PamFlag,
) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
/// This function is used to (re-)set the authentication token of the user.
///
/// The PAM library calls this function twice in succession. The first time with
/// PAM_PRELIM_CHECK and then, if the module does not return PAM_TRY_AGAIN, subsequently with
/// PAM_UPDATE_AUTHTOK. It is only on the second call that the authorization token is
/// (possibly) changed.
fn sm_chauthtok(
pamh: &PamHandle,
args: Vec<&CStr>,
flags: PamFlag,
) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
/// This function is called to terminate a session.
fn sm_close_session(
pamh: &PamHandle,
args: Vec<&CStr>,
flags: PamFlag,
) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
/// This function is called to commence a session.
fn sm_open_session(
pamh: &PamHandle,
args: Vec<&CStr>,
flags: PamFlag,
) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
/// This function performs the task of altering the credentials of the user with respect to the
/// corresponding authorization scheme. Generally, an authentication module may have access to more
/// information about a user than their authentication token. This function is used to make such
/// information available to the application. It should only be called after the user has been
/// authenticated but before a session has been established.
fn sm_setcred(
pamh: &PamHandle,
args: Vec<&CStr>,
flags: PamFlag,
) -> PamResultCode {
PamResultCode::PAM_IGNORE
}
}

15
rust/param/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "param"
edition.workspace = true
license.workspace = true
homepage.workspace = true
version.workspace = true
[dependencies]
chelps = { workspace = true }
dbg.workspace = true
ntstatus_gen.workspace = true
paste = "1.0.15"
[build-dependencies]
bindgen = "0.69.4"

53
rust/param/build.rs Normal file
View File

@ -0,0 +1,53 @@
use std::env;
use std::path::PathBuf;
fn main() {
let bindings = bindgen::Builder::default()
.blocklist_function("qgcvt")
.blocklist_function("qgcvt_r")
.blocklist_function("qfcvt")
.blocklist_function("qfcvt_r")
.blocklist_function("qecvt")
.blocklist_function("qecvt_r")
.blocklist_function("strtold")
.clang_arg("-Dbool=int")
.clang_arg("-Doffset_t=loff_t")
.clang_arg("-I../../bin/default")
.clang_arg("-I../../lib/talloc")
.generate_comments(false)
.clang_arg("-includestdint.h")
.header("../../lib/param/param.h")
.header("../../lib/param/loadparm.h")
.header("../../source3/param/loadparm.h")
.header("../../bin/default/lib/param/param_functions.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
println!("cargo:rerun-if-changed=../../lib/param/param.h");
println!("cargo:rerun-if-changed=../../lib/param/loadparm.h");
println!("cargo:rerun-if-changed=../../source3/param/loadparm.h");
println!(
"cargo:rerun-if-changed=../../bin/default/lib/param/param_functions.h"
);
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
let mut src_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
src_dir.push("../../bin/default/source3");
println!(
"cargo:rustc-link-search=native={}",
src_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=smbconf");
let mut src_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
src_dir.push("../../bin/default/lib/param");
println!("cargo:rustc-link-lib=samba-hostconfig-private-samba");
println!(
"cargo:rustc-link-search=native={}",
src_dir.to_str().unwrap()
);
}

201
rust/param/src/lib.rs Normal file
View File

@ -0,0 +1,201 @@
/*
Unix SMB/CIFS implementation.
Parameter loading functions
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 chelps::{string_free, wrap_c_char, wrap_string};
use dbg::{DBG_ERR, DBG_INFO, DBG_WARNING};
use ntstatus_gen::NT_STATUS_UNSUCCESSFUL;
use std::error::Error;
use std::ffi::c_void;
use std::ptr;
use std::sync::{Arc, Mutex};
mod ffi {
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(clippy::upper_case_acronyms)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
pub struct LoadParm {
lp: Arc<Mutex<*mut ffi::loadparm_context>>,
}
macro_rules! lpcfg_str {
($var:ident) => {
paste::item! {
pub fn $var (&self) -> Result<Option<String>, Box<dyn Error + '_>> {
let lp = self.lp.lock()?;
let val = unsafe { ffi::[< lpcfg_ $var >] (*lp) } ;
unsafe { Ok(wrap_c_char(val)) }
}
}
};
}
macro_rules! lpcfg_i32 {
($var:ident) => {
paste::item! {
pub fn $var (&self) -> Result<i32, Box<dyn Error + '_>> {
let lp = self.lp.lock()?;
unsafe { Ok(ffi::[< lpcfg_ $var >] (*lp)) }
}
}
};
}
macro_rules! lpcfg_bool {
($var:ident) => {
paste::item! {
pub fn $var (&self) -> Result<bool, Box<dyn Error + '_>> {
let lp = self.lp.lock()?;
unsafe { Ok(ffi::[< lpcfg_ $var >] (*lp) != 0) }
}
}
};
}
impl LoadParm {
pub fn new(configfile: Option<&str>) -> Result<Self, Box<dyn Error + '_>> {
let lp = unsafe {
match configfile {
Some(configfile) => {
let configfile_cstr = wrap_string(configfile);
let lp = ffi::loadparm_init_global(0);
if ffi::lpcfg_load(lp, configfile_cstr) != 1 {
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
string_free(configfile_cstr);
lp
}
None => ffi::loadparm_init_global(1),
}
};
Ok(LoadParm {
lp: Arc::new(Mutex::new(lp)),
})
}
pub fn private_path(
&self,
name: &str,
) -> Result<Option<String>, Box<dyn Error + '_>> {
let lp = self.lp.lock()?;
let path = unsafe {
let name_cstr = wrap_string(name);
let path =
ffi::lpcfg_private_path(*lp as *mut c_void, *lp, name_cstr);
string_free(name_cstr);
path
};
unsafe { Ok(wrap_c_char(path)) }
}
pub fn logfile(&self) -> Result<Option<String>, Box<dyn Error + '_>> {
let lp = self.lp.lock()?;
let logfile = unsafe {
let lp_sub = ffi::lpcfg_noop_substitution();
ffi::lpcfg_logfile(*lp, lp_sub, *lp as *mut c_void)
};
unsafe { Ok(wrap_c_char(logfile)) }
}
pub fn idmap_range(
&self,
domain_name: &str,
) -> Result<(u32, u32), Box<dyn Error + '_>> {
if let Ok(Some(backend)) = self.idmap_backend(domain_name) {
if backend != "upn" {
DBG_ERR!("Backend '{}' is not supported for Entra ID", backend);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
} else if domain_name != "*" {
DBG_WARNING!(
"No idmap backend configured for domain '{}'",
domain_name
);
DBG_INFO!("Falling back to default idmap configuration");
return self.idmap_range("*");
} else {
DBG_WARNING!("No idmap backend configured");
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
let lp = self.lp.lock()?;
let parm_name = format!("idmap config {}", domain_name);
let parm_opt = "range";
let range = unsafe {
let parm_name_cstr = wrap_string(&parm_name);
let parm_opt_cstr = wrap_string(parm_opt);
let parm_opt_value = ffi::lpcfg_parm_string(
*lp,
ptr::null_mut(),
parm_name_cstr,
parm_opt_cstr,
);
string_free(parm_name_cstr);
string_free(parm_opt_cstr);
wrap_c_char(parm_opt_value)
}
.ok_or(Box::new(NT_STATUS_UNSUCCESSFUL))?;
let parts: Vec<&str> = range.split('-').collect();
if parts.len() != 2 {
DBG_ERR!("Failed to parse the idmap range: {}", range);
return Err(Box::new(NT_STATUS_UNSUCCESSFUL));
}
Ok((parts[0].parse::<u32>()?, parts[1].parse::<u32>()?))
}
pub fn idmap_backend(
&self,
domain_name: &str,
) -> Result<Option<String>, Box<dyn Error + '_>> {
let lp = self.lp.lock()?;
let parm_name = format!("idmap config {}", domain_name);
let parm_opt = "backend";
unsafe {
let parm_name_cstr = wrap_string(&parm_name);
let parm_opt_cstr = wrap_string(parm_opt);
let parm_opt_value = ffi::lpcfg_parm_string(
*lp,
ptr::null_mut(),
parm_name_cstr,
parm_opt_cstr,
);
string_free(parm_name_cstr);
string_free(parm_opt_cstr);
Ok(wrap_c_char(parm_opt_value))
}
}
lpcfg_str!(realm);
lpcfg_str!(winbindd_socket_directory);
lpcfg_i32!(winbind_request_timeout);
lpcfg_bool!(himmelblaud_sfa_fallback);
lpcfg_bool!(himmelblaud_hello_enabled);
lpcfg_str!(cache_directory);
lpcfg_str!(template_homedir);
lpcfg_str!(template_shell);
lpcfg_str!(himmelblaud_hsm_pin_path);
}
unsafe impl Send for LoadParm {}
unsafe impl Sync for LoadParm {}

Some files were not shown because too many files have changed in this diff Show More