346 lines
8.5 KiB
Rust
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,
|
|
})
|
|
}
|