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}