libpam_sys_impls/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2//! An enumeration of PAM implementations and tools to detect them.
3//!
4//! # Configuration
5//!
6//! When used at compile time, this crate uses the target OS by default,
7//! but can be overridden with the `LIBPAMSYS_IMPL` environment variable.
8//! See the documentation of [`build_target_impl`] for details.
9//!
10//! # Detecting PAM
11//!
12//! ## Build time
13//!
14//! Use [`enable_pam_impl_cfg`] in your `build.rs` to generate custom `#[cfg]`s
15//! for conditional compilation based on PAM implementation.
16//!
17//! To detect the implementation that will be used at runtime, use the
18//! [`build_target_impl`] function.
19//!
20//! ## Run time
21//!
22//! The implementation of PAM installed on the machine where the code is running
23//! can be detected with [`currently_installed`], or you can use
24//! [`os_default`] to see what implementation is used on a given target.
25
26use std::env;
27use std::env::VarError;
28use std::ffi::c_void;
29use std::ptr::NonNull;
30
31/// An enum that knows its own values.
32macro_rules! self_aware_enum {
33    (
34        $(#[$enumeta:meta])*
35        $viz:vis enum $name:ident {
36            $(
37                $(#[$itemeta:meta])*
38                $item:ident,
39            )*
40        }
41    ) => {
42        $(#[$enumeta])*
43        $viz enum $name {
44            $(
45                $(#[$itemeta])*
46                $item,
47            )*
48        }
49
50        // The implementations in this block are private for now
51        // to avoid putting a contract into the public API.
52        #[allow(dead_code)]
53        impl $name {
54            /// Iterator over the items in the enum. For internal use.
55            pub(crate) fn items() -> Vec<Self> {
56                vec![$(Self::$item),*]
57            }
58
59            /// Attempts to parse the enum from the string. For internal use.
60            pub(crate) fn try_from(value: &str) -> Result<Self, String> {
61                match value {
62                    $(stringify!($item) => Ok(Self::$item),)*
63                    _ => Err(value.into()),
64                }
65            }
66        }
67    };
68}
69
70self_aware_enum! {
71    /// The PAM implementations supported by `libpam-sys`.
72    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73    #[non_exhaustive]
74    pub enum PamImpl {
75        /// [Linux-PAM] is provided by most Linux distributions.
76        ///
77        /// [Linux-PAM]: https://github.com/linux-pam/linux-pam
78        LinuxPam,
79        /// [OpenPAM] is used by most BSDs, including Mac OS X.
80        ///
81        /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM
82        OpenPam,
83        /// Illumos and Solaris use a derivative of [Sun's implementation][sun].
84        ///
85        /// [sun]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam
86        Sun,
87        /// Only the functionality and constants in [the PAM spec].
88        ///
89        /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm
90        XSso,
91    }
92}
93
94#[allow(clippy::needless_doctest_main)]
95/// Generates `cargo` directives for build scripts to enable `cfg(pam_impl)`.
96///
97/// Print this in your `build.rs` script to be able to use the custom `pam_impl`
98/// configuration directive.
99///
100/// ```
101/// // Your package's build.rs:
102///
103/// fn main() {
104///     // Also available at libpam_sys::pam_impl::enable_pam_impl_cfg().
105///     libpam_sys_impls::enable_pam_impl_cfg();
106///     // whatever else you do in your build script.
107/// }
108/// ```
109///
110/// This will set the current `pam_impl` as well as registering all known
111/// PAM implementations with `rustc-check-cfg` to get cfg-checking.
112///
113/// The names that appear in the `cfg` variables are the same as the values
114/// in the [`PamImpl`] enum.
115///
116/// ```ignore
117/// #[cfg(pam_impl = "OpenPam")]
118/// fn openpam_specific_func(handle: *const libpam_sys::pam_handle) {
119///     let environ = libpam_sys::pam_getenvlist(handle);
120///     // ...
121///     libpam_sys::openpam_free_envlist()
122/// }
123///
124/// // This will give you a warning since "UnknownImpl" is not a known
125/// // PAM implementation.
126/// #[cfg(not(pam_impl = "UnknownImpl"))]
127/// fn do_something() {
128///     // ...
129/// }
130/// ```
131pub fn enable_pam_impl_cfg() {
132    println!("{}", pam_impl_cfg_string())
133}
134
135/// [`enable_pam_impl_cfg`], but returned as a string.
136pub fn pam_impl_cfg_string() -> String {
137    generate_cfg(build_target_impl())
138}
139
140fn generate_cfg(pam_impl: Option<PamImpl>) -> String {
141    let impls: Vec<_> = PamImpl::items()
142        .into_iter()
143        .map(|i| format!(r#""{i:?}""#))
144        .collect();
145    let mut lines = vec![
146        format!(
147            "cargo:rustc-check-cfg=cfg(pam_impl, values({impls}))",
148            impls = impls.join(",")
149        ),
150        "cargo:rustc-cfg=pam_impl".into(),
151    ];
152    if let Some(pam_impl) = pam_impl {
153        lines.push("cargo:rustc-cfg=pam_impl".into());
154        lines.push(format!("cargo:rustc-cfg=pam_impl=\"{pam_impl:?}\""));
155    }
156    lines.join("\n")
157}
158
159/// The strategy to use to detect PAM.
160enum Detect {
161    /// Use the default PAM implementation based on the target OS.
162    TargetDefault,
163    /// Detect the installed implementation.
164    Installed,
165    /// Use the named version of PAM.
166    Specified(PamImpl),
167}
168
169const INSTALLED: &str = "__installed__";
170
171/// For `build.rs` use: Detects the PAM implementation that should be used
172/// for the target of the currently-running build script.
173///
174/// # Configuration
175///
176/// The PAM implementation selected depends upon the value of the
177/// `LIBPAMSYS_IMPL` environment variable.
178///
179/// - Empty or unset (default): Use the default PAM implementation for the
180///   Cargo target OS (as specified by `CARGO_CFG_TARGET_OS`).
181///   - Linux: Linux-PAM
182///   - BSD (and Mac): OpenPAM
183///   - Illumos/Solaris: Sun PAM
184/// - `__installed__`: Use the PAM implementation installed on the host system.
185///   This opens the `libpam` library and looks for specific functions.
186/// - The name of a [PamImpl] member: Use that PAM implementation.
187///
188/// # Panics
189///
190/// If an unknown PAM implementation is provided in `LIBPAMSYS_IMPL`.
191pub fn build_target_impl() -> Option<PamImpl> {
192    let detection = match env::var("LIBPAMSYS_IMPL").as_deref() {
193        Ok("") | Err(VarError::NotPresent) => Detect::TargetDefault,
194        Ok(INSTALLED) => Detect::Installed,
195        Ok(val) => Detect::Specified(PamImpl::try_from(val).unwrap_or_else(|_| {
196            panic!(
197                "unknown PAM implementation {val:?}. \
198                valid LIBPAMSYS_IMPL values are {:?}, \
199                {INSTALLED:?} to use the currently-installed version, \
200                or unset to use the OS default",
201                PamImpl::items()
202            )
203        })),
204        Err(other) => panic!("Couldn't detect PAM version: {other}"),
205    };
206    match detection {
207        Detect::TargetDefault => env::var("CARGO_CFG_TARGET_OS")
208            .ok()
209            .as_deref()
210            .and_then(os_default),
211        Detect::Installed => currently_installed(),
212        Detect::Specified(other) => Some(other),
213    }
214}
215
216/// Gets the PAM version based on the target OS.
217///
218/// The target OS name passed in is one of the [Cargo target OS values][os].
219///
220/// [os]: https://doc.rust-lang.org/reference/conditional-compilation.html#r-cfg.target_os.values
221pub fn os_default(target_os: &str) -> Option<PamImpl> {
222    match target_os {
223        "linux" => Some(PamImpl::LinuxPam),
224        "macos" | "freebsd" | "netbsd" | "dragonfly" | "openbsd" => Some(PamImpl::OpenPam),
225        "illumos" | "solaris" => Some(PamImpl::Sun),
226        _ => None,
227    }
228}
229
230/// The version of LibPAM installed on this machine (as found by `dlopen`).
231pub fn currently_installed() -> Option<PamImpl> {
232    LibPam::open().map(|lib| {
233        if lib.has(b"pam_syslog\0") {
234            PamImpl::LinuxPam
235        } else if lib.has(b"_openpam_log\0") {
236            PamImpl::OpenPam
237        } else if lib.has(b"__pam_get_authtok\0") {
238            PamImpl::Sun
239        } else {
240            PamImpl::XSso
241        }
242    })
243}
244
245struct LibPam(NonNull<c_void>);
246
247impl LibPam {
248    fn open() -> Option<Self> {
249        let dlopen = |s: &[u8]| unsafe { libc::dlopen(s.as_ptr().cast(), libc::RTLD_LAZY) };
250        NonNull::new(dlopen(b"libpam.so\0"))
251            .or_else(|| NonNull::new(dlopen(b"libpam.dylib\0")))
252            .map(Self)
253    }
254
255    fn has(&self, name: &[u8]) -> bool {
256        let symbol = unsafe { libc::dlsym(self.0.as_ptr(), name.as_ptr().cast()) };
257        !symbol.is_null()
258    }
259}
260
261impl Drop for LibPam {
262    fn drop(&mut self) {
263        unsafe {
264            libc::dlclose(self.0.as_ptr());
265        }
266    }
267}