init
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
commit
792d15fef5
7 changed files with 1812 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1560
Cargo.lock
generated
Normal file
1560
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "pam-apppasswd-hashicorp-vault"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.9", default-features = false, features = ["charset", "json", "blocking", "rustls-tls", "rustls-tls-native-roots"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.70.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
31
build.rs
Normal file
31
build.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[allow(deprecated, reason = "False positive")]
|
||||
use bindgen::CargoCallbacks;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-lib=pam");
|
||||
|
||||
// The bindgen::Builder is the main entry point
|
||||
// to bindgen, and lets you build up options for
|
||||
// the resulting bindings.
|
||||
let bindings = bindgen::Builder::default()
|
||||
// The input header we would like to generate
|
||||
// bindings for.
|
||||
.header("wrapper.h")
|
||||
.default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
|
||||
// Tell cargo to invalidate the built crate whenever any of the
|
||||
// included header files changed.
|
||||
.parse_callbacks(Box::new(CargoCallbacks::new()))
|
||||
// Finish the builder and generate the bindings.
|
||||
.generate()
|
||||
// Unwrap the Result and panic on failure.
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
// Write the bindings to the $OUT_DIR/bindings.rs file.
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("pam_sys.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
195
src/lib.rs
Normal file
195
src/lib.rs
Normal file
|
@ -0,0 +1,195 @@
|
|||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use pam_sys::{pam_handle_t, PAM_ABORT, PAM_AUTH_ERR, PAM_SUCCESS};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod pam_sys;
|
||||
|
||||
macro_rules! check_result {
|
||||
($e:expr) => {{
|
||||
let ret = $e;
|
||||
match ret {
|
||||
Ok(r) => r,
|
||||
Err(ret) => {
|
||||
eprintln!("Error: {:?}", ret);
|
||||
return PAM_ABORT;
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub struct Environment {
|
||||
vault_url: String,
|
||||
vault_token: String,
|
||||
mount_point: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct VaultResponse<D> {
|
||||
data: D,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct KVData {
|
||||
data: Option<KVDataData>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct KVDataData {
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn from_env() -> Result<Self, std::env::VarError> {
|
||||
let vault_url = std::env::var("VAULT_ADDR")?;
|
||||
let vault_token = std::env::var("VAULT_TOKEN")?;
|
||||
let mount_point = std::env::var("VAULT_MOUNT_POINT")?;
|
||||
|
||||
Ok(Self {
|
||||
vault_url,
|
||||
vault_token,
|
||||
mount_point,
|
||||
})
|
||||
}
|
||||
pub fn from_env_file(file: &str) -> Result<Self, std::io::Error> {
|
||||
let f = std::fs::read_to_string(file)?;
|
||||
|
||||
let mut vault_url = String::new();
|
||||
let mut vault_token = String::new();
|
||||
let mut mount_point = String::new();
|
||||
|
||||
for line in f.lines().filter(|l| !l.is_empty() && !l.starts_with('#')) {
|
||||
let (key, value) = line.split_once('=').unwrap_or((line, ""));
|
||||
|
||||
match key {
|
||||
"VAULT_ADDR" => vault_url = value.trim().to_string(),
|
||||
"VAULT_TOKEN" => vault_token = value.trim().to_string(),
|
||||
"VAULT_MOUNT_POINT" => mount_point = value.trim().to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
vault_url,
|
||||
vault_token,
|
||||
mount_point,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn check_vault(
|
||||
env: &Environment,
|
||||
user: &str,
|
||||
key: &str,
|
||||
token: &str,
|
||||
) -> Result<bool, reqwest::Error> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/v1/{}/data/{}/{}",
|
||||
env.vault_url,
|
||||
env.mount_point,
|
||||
path_encode(user),
|
||||
key
|
||||
);
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", env.vault_token.as_str())
|
||||
.send()?
|
||||
.error_for_status()?
|
||||
.json::<VaultResponse<KVData>>()?;
|
||||
|
||||
if let Some(data) = res.data.data {
|
||||
if let Some(password) = data.password {
|
||||
return Ok(password == token);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn path_encode(path: &str) -> String {
|
||||
path.chars()
|
||||
.map(|c| match c {
|
||||
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => c.to_string(),
|
||||
_ => format!("%{:02X}", c as u8),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn valid_user(user: &str) -> bool {
|
||||
user.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "_-.@".contains(c))
|
||||
}
|
||||
|
||||
fn valid_token(token: &str) -> bool {
|
||||
token
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "!@#$%^&*()_+-=".contains(c))
|
||||
}
|
||||
|
||||
fn valid_token_key(token_key: &str) -> bool {
|
||||
token_key
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "_".contains(c))
|
||||
}
|
||||
|
||||
/// This function is called by PAM to authenticate the user.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function must be called by PAM with the correct arguments defined by the PAM API.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn pam_sm_authenticate(
|
||||
_pamh: *mut pam_handle_t,
|
||||
_flags: c_int,
|
||||
arg_count: c_int,
|
||||
arg_value: *const *const c_char,
|
||||
) -> c_int {
|
||||
let user = check_result!(std::env::var("PAM_USER"));
|
||||
|
||||
let token = check_result!(std::env::var("PAM_AUTHTOK"));
|
||||
|
||||
let (token_key, token_value) = token.split_once('=').unwrap_or_else(|| ("default", &token));
|
||||
|
||||
if user.is_empty()
|
||||
|| token_value.len() < 10
|
||||
|| !valid_token_key(token_key)
|
||||
|| !valid_user(&user)
|
||||
|| !valid_token(&token)
|
||||
{
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
let mut vault_conf: *mut c_char = std::ptr::null_mut();
|
||||
|
||||
for i in 0..arg_count {
|
||||
let arg = *arg_value.offset(i as isize);
|
||||
let arg = check_result!(std::ffi::CStr::from_ptr(arg).to_str());
|
||||
|
||||
if arg.starts_with("vault_conf=") {
|
||||
vault_conf = arg.trim_start_matches("vault_conf=").as_ptr() as *mut c_char;
|
||||
}
|
||||
}
|
||||
|
||||
if vault_conf.is_null() {
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
let vault_conf = check_result!(std::ffi::CStr::from_ptr(vault_conf).to_str());
|
||||
|
||||
let env = check_result!(Environment::from_env_file(vault_conf));
|
||||
|
||||
match check_vault(&env, &user, token_key, token_value) {
|
||||
Ok(true) => PAM_SUCCESS,
|
||||
Ok(false) => PAM_AUTH_ERR,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e:?}");
|
||||
PAM_ABORT
|
||||
}
|
||||
}
|
||||
}
|
5
src/pam_sys.rs
Normal file
5
src/pam_sys.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#![allow(clippy::all)]
|
||||
#![allow(clippy::pedantic)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/pam_sys.rs"));
|
1
wrapper.h
Normal file
1
wrapper.h
Normal file
|
@ -0,0 +1 @@
|
|||
#include <security/pam_modules.h>
|
Loading…
Add table
Reference in a new issue