ldap_backend/src/lib.rs

346 lines
8.5 KiB
Rust

pub mod methods;
use chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv;
use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
Error, Pool, Sqlite,
};
use std::{
env,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use tide::prelude::*;
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct AccountWolves {
pub id_wolves: i64,
pub id_student: Option<String>,
pub email: String,
pub expiry: String,
pub name_first: Option<String>,
pub name_second: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]
pub struct AccountsNew {
pub mail: String,
pub auth_code: String,
pub date_iso: String,
pub date_expiry: String,
pub name_first: String,
pub name_surname: String,
pub id_student: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]
pub struct AccountsReset {
pub user: String,
pub auth_code: String,
pub date_expiry: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]
pub struct AccountsSSH {
pub user: String,
pub auth_code: String,
pub email: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]
pub struct Accounts {
pub user: String,
pub uid: i64,
pub mail: String,
pub student_id: String,
pub secure: bool,
pub id_wolves: i64,
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true)
.create_if_missing(true),
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db").run(&pool).await?;
Ok(pool)
}
pub fn get_now() -> i64 {
if let Ok(x) = SystemTime::now().duration_since(UNIX_EPOCH) {
x.as_secs() as i64
} else {
0
}
}
pub fn get_now_iso(short: bool) -> String {
let now = Utc::now();
if short {
format!("{}-{:02}-{:02}", now.year(), now.month(), now.day())
} else {
now.to_rfc3339_opts(SecondsFormat::Millis, true)
}
}
#[derive(Clone)]
pub struct State {
pub db: Pool<Sqlite>,
pub config: Config,
}
#[derive(Debug, Clone)]
pub struct Config {
pub ldap_host: String,
pub ldap_admin: String,
pub ldap_admin_pw: String,
pub home: String,
pub database: String,
pub host_port: String,
pub mail_smtp: String,
pub mail_user: String,
pub mail_pass: String,
pub ssh_root: String,
pub users_restricted: Vec<String>,
pub wolves_url: String,
pub wolves_key: String,
}
pub fn get_config() -> Config {
dotenv().ok();
// reasonable defaults
let mut config = Config {
ldap_host: "".to_string(),
ldap_admin: "".to_string(),
ldap_admin_pw: "".to_string(),
home: ".".to_string(),
database: "database.db".to_string(),
host_port: "127.0.0.1:8087".to_string(),
mail_smtp: "".to_string(),
mail_user: "".to_string(),
mail_pass: "".to_string(),
ssh_root: "skynet_old".to_string(),
users_restricted: vec![],
wolves_url: "".to_string(),
wolves_key: "".to_string(),
};
if let Ok(x) = env::var("LDAP_HOST") {
config.ldap_host = x.trim().to_string();
}
if let Ok(x) = env::var("LDAP_ADMIN") {
config.ldap_admin = x.trim().to_string();
}
if let Ok(x) = env::var("LDAP_ADMIN_PW") {
config.ldap_admin_pw = x.trim().to_string();
}
if let Ok(x) = env::var("DATABASE_HOME") {
config.home = x.trim().to_string();
}
if let Ok(x) = env::var("DATABASE") {
config.database = x.trim().to_string();
}
if let Ok(x) = env::var("HOST_PORT") {
config.host_port = x.trim().to_string();
}
if let Ok(x) = env::var("EMAIL_SMTP") {
config.mail_smtp = x.trim().to_string();
}
if let Ok(x) = env::var("EMAIL_USER") {
config.mail_user = x.trim().to_string();
}
if let Ok(x) = env::var("EMAIL_PASS") {
config.mail_pass = x.trim().to_string();
}
if let Ok(x) = env::var("SSH_ROOT") {
config.ssh_root = x.trim().to_string();
}
if let Ok(x) = env::var("WOLVES_URL") {
config.wolves_url = x.trim().to_string();
}
if let Ok(x) = env::var("WOLVES_KEY") {
config.wolves_key = x.trim().to_string();
}
if let Ok(x) = env::var("USERS_RESTRICTED") {
// usernames that are restricted
for user in x.split(',').collect::<Vec<&str>>() {
config.users_restricted.push(user.to_string());
}
}
config
}
// from https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html#create-random-passwords-from-a-set-of-alphanumeric-characters
pub fn random_string(len: usize) -> String {
thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}
pub async fn get_wolves(db: &Pool<Sqlite>) -> Vec<AccountWolves> {
sqlx::query_as::<_, AccountWolves>(
r#"
SELECT *
FROM accounts_wolves
"#,
)
.fetch_all(db)
.await
.unwrap_or(vec![])
}
pub fn uid_to_dn(uid: &str) -> String {
format!("uid={},ou=users,dc=skynet,dc=ie", uid)
}
pub async fn update_group(config: &Config, group: &str, users: &[String], replace: bool) -> tide::Result<()> {
if users.is_empty() {
return Ok(());
}
let mut ldap = LdapConn::new(&config.ldap_host)?;
// use the admin account
ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?;
let dn = format!("cn={},ou=groups,dc=skynet,dc=ie", group);
let members = users.iter().map(|uid| uid_to_dn(uid)).collect();
let mods = if replace {
vec![Mod::Replace("member".to_string(), members)]
} else {
vec![Mod::Add("member".to_string(), members)]
};
if let Err(x) = ldap.modify(&dn, mods) {
println!("{:?}", x);
}
let dn_linux = format!("cn={}-linux,ou=groups,dc=skynet,dc=ie", group);
let members_linux = users.iter().map(|uid| uid.to_string()).collect();
let mods = if replace {
vec![Mod::Replace("memberUid".to_string(), members_linux)]
} else {
vec![Mod::Add("memberUid".to_string(), members_linux)]
};
if let Err(x) = ldap.modify(&dn_linux, mods) {
println!("{:?}", x);
};
// tidy up
ldap.unbind()?;
Ok(())
}
#[derive(Debug, Deserialize)]
pub struct LdapAuth {
user: String,
pass: String,
}
#[derive(Debug)]
pub struct LdapAuthResult {
ldap: LdapConn,
dn: String,
is_skynet_user: bool,
}
/// Auths and updates the users password hash
pub async fn auth_user(auth: &LdapAuth, config: &Config) -> Option<LdapAuthResult> {
let LdapAuth {
user,
pass,
} = auth;
// easier to give each request its own connection
let mut ldap = match LdapConn::new(&config.ldap_host) {
Ok(x) => x,
Err(err) => {
println!("{:?}", err);
return None;
}
};
let dn = format!("uid={},ou=users,dc=skynet,dc=ie", user);
// authenticate with the users own apssword
match ldap.simple_bind(&dn, pass) {
Ok(result) => match result.success() {
Ok(_) => {}
Err(err) => {
println!("{:?}", err);
return None;
}
},
Err(err) => {
println!("{:?}", err);
return None;
}
}
// always assume insecure
let mut pw_keep_same = false;
let mut is_skynet_user = false;
// get the users current password hash
if let Ok(result) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword"]) {
if let Ok((rs, _res)) = result.success() {
if !rs.is_empty() {
let tmp = SearchEntry::construct(rs[0].clone());
if tmp.attrs.contains_key("userPassword") && !tmp.attrs["userPassword"].is_empty() && tmp.attrs["userPassword"][0].starts_with("{SSHA512}") {
pw_keep_same = true;
}
if tmp.attrs.contains_key("memberOf") {
for group in tmp.attrs["memberOf"].clone() {
if group.contains("skynet-users") {
is_skynet_user = true;
}
}
}
}
}
}
if !pw_keep_same {
let tmp = PasswordModify {
// No need to set the id, since we are already authed for this
user_id: None,
old_pass: Some(pass),
// although the same as the old it will allow it to be re-hashed to SSHA512
new_pass: Some(pass),
};
match ldap.extended(tmp) {
Ok(x) => {
match x.success() {
Ok(_) => {}
Err(err) => {
println!("{:?}", err);
// not returning None as the user still managed to auth
}
}
}
Err(err) => {
println!("{:?}", err);
}
}
}
Some(LdapAuthResult {
ldap,
dn,
is_skynet_user,
})
}