intial commit

This commit is contained in:
2026-02-25 22:53:25 +01:00
commit 054c3ff1f8
12 changed files with 1164 additions and 0 deletions

62
src/args.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::{
os::{linux::net::SocketAddrExt, unix::net::SocketAddr},
path::PathBuf,
};
use clap::Parser;
/// OpenID Connect server with unix backend
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(styles = get_clap_styles())]
pub struct Args {
#[arg(short, default_value = "/etc/authy-oidc.toml")]
pub conf: PathBuf,
#[cfg(feature = "pamsock")]
#[arg(long, default_value = "pam")]
#[arg(value_parser = parse_as_abstract_name)]
pub pamsock_abstract_name: std::os::unix::net::SocketAddr,
}
fn parse_as_abstract_name(name: &str) -> std::io::Result<SocketAddr> {
SocketAddr::from_abstract_name(name.as_bytes())
}
const fn get_clap_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
.usage(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(208)))),
)
.header(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(208)))),
)
.literal(
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
)
.invalid(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
)
.error(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
)
.valid(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
)
.placeholder(
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
)
}

18
src/auth/mod.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::fmt::{Debug, Display};
#[derive(Debug)]
pub enum AuthenticateResponse<F: Display + Debug> {
/// All rightie
Success,
/// The message will be displayed to the frontend, must give a reason of why the user login
/// failed
Failed(F),
}
#[cfg(feature = "pamsock")]
pub mod pamsock;
#[cfg(feature = "pamsock")]
use pamsock as auth;
pub use auth::authenticate;

40
src/auth/pamsock.rs Normal file
View File

@@ -0,0 +1,40 @@
//! Horrible error handling here btw
use std::fmt::{Debug, Display};
use pamsock::prot::ServerResponse;
use super::AuthenticateResponse;
#[allow(clippy::from_over_into)]
impl Into<AuthenticateResponse<&'static str>> for ServerResponse {
fn into(self) -> AuthenticateResponse<&'static str> {
use AuthenticateResponse as AR;
match self {
Self::ServerError => AR::Failed("unknown server error"),
Self::Locked => AR::Failed("account locked, too many login attempts"),
Self::Failed => AR::Failed("wrong credentials"),
Self::Succeeded => AR::Success,
}
}
}
pub async fn authenticate(
cfg: &crate::args::Args,
user: &str,
passwd: &str,
) -> Option<AuthenticateResponse<impl Display + Debug>> {
use std::os::unix::net::UnixStream;
use tokio::net::UnixStream as AsyncUnixStream;
let std_sock = UnixStream::connect_addr(&cfg.pamsock_abstract_name).ok()?;
std_sock.set_nonblocking(true).ok();
let async_sock = AsyncUnixStream::from_std(std_sock).ok()?;
Some(
pamsock::prot::attempt_login_async(async_sock, user, passwd)
.await?
.into(),
)
}

64
src/conf.rs Normal file
View File

@@ -0,0 +1,64 @@
use std::{
fmt::Debug,
fs::File,
io::{self, Read, Seek},
};
use libc::gid_t;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum Either<T, U> {
Left(T),
Right(U),
}
impl<T: Debug, U: Debug> Debug for Either<T, U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Left(l) => l.fmt(f),
Self::Right(r) => r.fmt(f),
}
}
}
pub type Group = Either<String, gid_t>;
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Unix {
groups: Vec<Group>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
unix: Unix,
}
#[derive(thiserror::Error, Debug)]
pub enum ConfigParseError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Parse(#[from] toml::de::Error),
}
impl Config {
/// # Errors
///
/// For IO errors reading the file or parsing errors.
pub fn from_toml_file(f: &mut File) -> Result<Self, ConfigParseError> {
let mut buf = Vec::new();
if let Ok(len) = f.stream_len() {
// There's no way a config file passes machine's memory limitations
#[allow(clippy::cast_possible_truncation)]
buf.reserve_exact(len as usize);
}
f.read_to_end(&mut buf)?;
Ok(toml::from_slice(&buf)?)
}
}

16
src/ext/mod.rs Normal file
View File

@@ -0,0 +1,16 @@
use std::{fs::File, io, path::Path};
pub trait FileExt {
/// Tries to [`File::open`] a file, but wrapped around an option if the file doesn't exist.
fn try_open<P: AsRef<Path>>(path: P) -> Option<io::Result<File>>;
}
impl FileExt for File {
fn try_open<P: AsRef<Path>>(path: P) -> Option<io::Result<File>> {
match Self::open(path) {
Ok(f) => Some(Ok(f)),
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => Some(Err(e)),
}
}
}

29
src/main.rs Normal file
View File

@@ -0,0 +1,29 @@
#![feature(seek_stream_len)]
use std::fs::File;
use clap::Parser;
use crate::ext::FileExt;
pub mod args;
pub mod auth;
pub mod conf;
pub mod ext;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = args::Args::parse();
let conf = if let Some(conf) = File::try_open(&args.conf) {
conf::Config::from_toml_file(&mut conf?)?
} else {
conf::Config::default()
};
println!("{conf:#?}");
let res = auth::authenticate(&args, "javalsai", "test").await;
println!("{res:?}");
Ok(())
}