Merge branch 'main' into #4-password-reset
# Conflicts: # src/bin/update_groups.rs
This commit is contained in:
commit
8f43e777f5
4 changed files with 157 additions and 63 deletions
14
README.md
14
README.md
|
@ -28,6 +28,20 @@ Fields:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Success:
|
||||||
|
Each value is either a string or ``null``.
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": "success",
|
||||||
|
"success": {
|
||||||
|
"cn": "Firstname Surname",
|
||||||
|
"mail": "Email address",
|
||||||
|
"skDiscord": null,
|
||||||
|
"sshPublicKey": "ssh key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Changing ``userPassword`` requires the existing password in teh apssword field and the new one in teh value field.
|
Changing ``userPassword`` requires the existing password in teh apssword field and the new one in teh value field.
|
||||||
|
|
||||||
### POST /ldap/new
|
### POST /ldap/new
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use dotenvy::dotenv;
|
|
||||||
use ldap3::{LdapConn, Mod};
|
use ldap3::{LdapConn, Mod};
|
||||||
use skynet_ldap_backend::{db_init, get_config, get_now_iso, read_csv, uid_to_dn, Accounts, Config};
|
use skynet_ldap_backend::{db_init, get_config, get_now_iso, read_csv, uid_to_dn, Accounts, Config};
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
|
@ -8,17 +7,18 @@ use std::{collections::HashSet, env, error::Error};
|
||||||
async fn main() -> tide::Result<()> {
|
async fn main() -> tide::Result<()> {
|
||||||
let config = get_config();
|
let config = get_config();
|
||||||
|
|
||||||
update_users(&config).await?;
|
update(&config).await?;
|
||||||
update_admin(&config).await?;
|
|
||||||
update_committee(&config).await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_users(config: &Config) -> tide::Result<()> {
|
async fn update(config: &Config) -> tide::Result<()> {
|
||||||
let mut users_tmp = HashSet::new();
|
let db = db_init(config).await.unwrap();
|
||||||
|
|
||||||
// default user to ensure group is never empty
|
// default user to ensure group is never empty
|
||||||
users_tmp.insert(String::from("compsoc"));
|
let mut users_tmp = HashSet::from([String::from("compsoc")]);
|
||||||
|
let mut admins_tmp = HashSet::from([String::from("compsoc")]);
|
||||||
|
let mut committee_tmp = HashSet::from([String::from("compsoc")]);
|
||||||
|
|
||||||
if let Ok(x) = env::var("USERS_LIFETIME") {
|
if let Ok(x) = env::var("USERS_LIFETIME") {
|
||||||
for user in x.split(',').collect::<Vec<&str>>() {
|
for user in x.split(',').collect::<Vec<&str>>() {
|
||||||
|
@ -31,6 +31,23 @@ async fn update_users(config: &Config) -> tide::Result<()> {
|
||||||
users_tmp.insert(user);
|
users_tmp.insert(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(x) = env::var("USERS_ADMIN") {
|
||||||
|
// admins automatically get added as users
|
||||||
|
for user in x.split(',').collect::<Vec<&str>>() {
|
||||||
|
admins_tmp.insert(user.to_string());
|
||||||
|
users_tmp.insert(user.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read from teh env
|
||||||
|
if let Ok(x) = env::var("USERS_COMMITTEE") {
|
||||||
|
// committee automatically get added as users
|
||||||
|
for user in x.split(',').collect::<Vec<&str>>() {
|
||||||
|
committee_tmp.insert(user.to_string());
|
||||||
|
users_tmp.insert(user.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sorting makes it easier/faster
|
// sorting makes it easier/faster
|
||||||
if let Ok(x) = env::var("USERS_BANNED") {
|
if let Ok(x) = env::var("USERS_BANNED") {
|
||||||
for user in x.split(',').collect::<Vec<&str>>() {
|
for user in x.split(',').collect::<Vec<&str>>() {
|
||||||
|
@ -38,43 +55,20 @@ async fn update_users(config: &Config) -> tide::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// easier to work with Strings above but easier to work with &str below
|
let AccountsSecure {
|
||||||
let users: Vec<&str> = users_tmp.iter().map(|s| &**s).collect();
|
users,
|
||||||
|
admins,
|
||||||
|
committee,
|
||||||
|
} = get_secure(&db, &users_tmp, &admins_tmp, &committee_tmp).await;
|
||||||
|
|
||||||
update_group(config, "skynet-users", &users, true).await?;
|
update_group(config, "skynet-users", &users, true).await?;
|
||||||
|
update_group(config, "skynet-admins", &admins, true).await?;
|
||||||
|
update_group(config, "skynet-committee", &committee, true).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_admin(config: &Config) -> tide::Result<()> {
|
async fn update_group(config: &Config, group: &str, users: &Vec<String>, replace: bool) -> tide::Result<()> {
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
// read from teh env
|
|
||||||
if let Ok(x) = env::var("USERS_ADMIN") {
|
|
||||||
let users = x.split(',').collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
update_group(config, "skynet-admins", &users, true).await?;
|
|
||||||
// admins automatically get added as users
|
|
||||||
update_group(config, "skynet-users", &users, false).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_committee(config: &Config) -> tide::Result<()> {
|
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
// read from teh env
|
|
||||||
if let Ok(x) = env::var("USERS_COMMITTEE") {
|
|
||||||
let users = x.split(',').collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
update_group(config, "skynet-committee", &users, true).await?;
|
|
||||||
// admins automatically get added as users
|
|
||||||
update_group(config, "skynet-users", &users, false).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_group(config: &Config, group: &str, users: &[&str], replace: bool) -> tide::Result<()> {
|
|
||||||
if users.is_empty() {
|
if users.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -181,3 +175,56 @@ async fn account_id_get_uid(db: &Pool<Sqlite>, id: &str) -> Option<String> {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AccountsSecure {
|
||||||
|
users: Vec<String>,
|
||||||
|
admins: Vec<String>,
|
||||||
|
committee: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_secure(db: &Pool<Sqlite>, users: &HashSet<String>, admins: &HashSet<String>, committee: &HashSet<String>) -> AccountsSecure {
|
||||||
|
// to avoid searching for teh same thing again.
|
||||||
|
let mut cache = HashSet::new();
|
||||||
|
AccountsSecure {
|
||||||
|
users: get_secure_sub(db, users, &mut cache).await,
|
||||||
|
admins: get_secure_sub(db, admins, &mut cache).await,
|
||||||
|
committee: get_secure_sub(db, committee, &mut cache).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_secure_sub(db: &Pool<Sqlite>, group: &HashSet<String>, cache: &mut HashSet<String>) -> Vec<String> {
|
||||||
|
let mut tmp = vec![];
|
||||||
|
|
||||||
|
for user in group {
|
||||||
|
// check the cache first
|
||||||
|
let mut add = false;
|
||||||
|
if cache.get(user).is_some() {
|
||||||
|
add = true;
|
||||||
|
} else if is_secure(db, user).await {
|
||||||
|
cache.insert(user.to_string());
|
||||||
|
add = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if add {
|
||||||
|
tmp.push(user.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
async fn is_secure(db: &Pool<Sqlite>, user: &str) -> bool {
|
||||||
|
match sqlx::query_as::<_, Accounts>(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM accounts
|
||||||
|
WHERE user == ? AND secure == 1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(res) => !res.is_empty(),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ async fn update_accounts(pool: &Pool<Sqlite>, config: &Config) {
|
||||||
ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw).unwrap().success().unwrap();
|
ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw).unwrap().success().unwrap();
|
||||||
|
|
||||||
// use this to pre load a large chunk of data
|
// use this to pre load a large chunk of data
|
||||||
if let Ok(x) = ldap.search("ou=users,dc=skynet,dc=ie", Scope::OneLevel, "(objectClass=*)", vec!["uid", "uidNumber", "skDiscord", "skMemberOf", "mail", "skID", "skSecure"]) {
|
if let Ok(x) = ldap.search("ou=users,dc=skynet,dc=ie", Scope::OneLevel, "(objectClass=*)", vec!["uid", "uidNumber", "skDiscord", "skMemberOf", "mail", "skID", "userPassword"]) {
|
||||||
if let Ok((rs, _res)) = x.success() {
|
if let Ok((rs, _res)) = x.success() {
|
||||||
for entry in rs {
|
for entry in rs {
|
||||||
let tmp = SearchEntry::construct(entry);
|
let tmp = SearchEntry::construct(entry);
|
||||||
|
@ -242,8 +242,8 @@ async fn update_accounts(pool: &Pool<Sqlite>, config: &Config) {
|
||||||
if tmp.attrs.contains_key("skMemberOf") && !tmp.attrs["skMemberOf"].is_empty() && tmp.attrs["skMemberOf"].contains(&String::from("cn=skynet-users-linux,ou=groups,dc=skynet,dc=ie")) {
|
if tmp.attrs.contains_key("skMemberOf") && !tmp.attrs["skMemberOf"].is_empty() && tmp.attrs["skMemberOf"].contains(&String::from("cn=skynet-users-linux,ou=groups,dc=skynet,dc=ie")) {
|
||||||
tmp_account.enabled = true;
|
tmp_account.enabled = true;
|
||||||
}
|
}
|
||||||
if tmp.attrs.contains_key("skSecure") && !tmp.attrs["skSecure"].is_empty() {
|
if tmp.attrs.contains_key("userPassword") && !tmp.attrs["userPassword"].is_empty() {
|
||||||
tmp_account.secure = true;
|
tmp_account.secure = tmp.attrs["userPassword"][0].starts_with("{SSHA512}")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tmp_account.user.is_empty() {
|
if !tmp_account.user.is_empty() {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::State;
|
use crate::State;
|
||||||
use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry};
|
use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry};
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use tide::{
|
use tide::{
|
||||||
prelude::{json, Deserialize},
|
prelude::{json, Deserialize, Serialize},
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,6 +14,16 @@ pub struct LdapUpdate {
|
||||||
value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ModifyResult {
|
||||||
|
mail: Option<String>,
|
||||||
|
#[serde(rename = "sshPublicKey")]
|
||||||
|
ssh_public_key: Option<String>,
|
||||||
|
cn: Option<String>,
|
||||||
|
#[serde(rename = "skDiscord")]
|
||||||
|
sk_discord: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles updating a single field with the users own password
|
/// Handles updating a single field with the users own password
|
||||||
pub async fn post_update_ldap(mut req: Request<State>) -> tide::Result {
|
pub async fn post_update_ldap(mut req: Request<State>) -> tide::Result {
|
||||||
let LdapUpdate {
|
let LdapUpdate {
|
||||||
|
@ -38,7 +48,6 @@ pub async fn post_update_ldap(mut req: Request<State>) -> tide::Result {
|
||||||
|
|
||||||
// always assume insecure
|
// always assume insecure
|
||||||
let mut pw_keep_same = false;
|
let mut pw_keep_same = false;
|
||||||
let mut pw_secure = false;
|
|
||||||
|
|
||||||
// get the users current password hash
|
// get the users current password hash
|
||||||
let (rs, _res) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword"])?.success()?;
|
let (rs, _res) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword"])?.success()?;
|
||||||
|
@ -46,52 +55,76 @@ pub async fn post_update_ldap(mut req: Request<State>) -> tide::Result {
|
||||||
let tmp = SearchEntry::construct(rs[0].clone());
|
let tmp = SearchEntry::construct(rs[0].clone());
|
||||||
if !tmp.attrs["userPassword"].is_empty() && tmp.attrs["userPassword"][0].starts_with("{SSHA512}") {
|
if !tmp.attrs["userPassword"].is_empty() && tmp.attrs["userPassword"][0].starts_with("{SSHA512}") {
|
||||||
pw_keep_same = true;
|
pw_keep_same = true;
|
||||||
pw_secure = true;
|
|
||||||
}
|
|
||||||
if tmp.attrs.contains_key("skSecure") && !tmp.attrs["skSecure"].is_empty() && tmp.attrs["skSecure"][0] == "1" {
|
|
||||||
pw_secure = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the password field itself is being updated
|
// check if the password field itself is being updated
|
||||||
let (pass_old, pass_new) = if &field != "userPassword" {
|
let pass_new = if &field != "userPassword" {
|
||||||
// if password is not being updated then just update the required field
|
// if password is not being updated then just update the required field
|
||||||
let mut mods = vec![
|
let mods = vec![
|
||||||
// main value we are updating
|
// the value we are updating
|
||||||
Mod::Replace(field, HashSet::from([value])),
|
Mod::Replace(field, HashSet::from([value])),
|
||||||
];
|
];
|
||||||
|
|
||||||
// if teh password is changing then its inherentrly secure, same if its currently an empty field
|
|
||||||
if !pw_keep_same || !pw_secure {
|
|
||||||
mods.push(Mod::Replace(String::from("skSecure"), HashSet::from([String::from("1")])));
|
|
||||||
}
|
|
||||||
|
|
||||||
ldap.modify(&dn, mods)?.success()?;
|
ldap.modify(&dn, mods)?.success()?;
|
||||||
|
|
||||||
// pass back the "old" and "new" passwords
|
// pass back the "old" and "new" passwords
|
||||||
(pass.clone(), pass.clone())
|
// using this means we can create teh vars without them needing to be mutable
|
||||||
|
pass.clone()
|
||||||
} else {
|
} else {
|
||||||
// password is going to be updated, even if the old value is not starting with "{SSHA512}"
|
// password is going to be updated, even if the old value is not starting with "{SSHA512}"
|
||||||
|
|
||||||
pw_keep_same = false;
|
pw_keep_same = false;
|
||||||
(pass.clone(), value)
|
value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// changing teh password because of an explicit request or upgrading teh security.
|
||||||
if !pw_keep_same {
|
if !pw_keep_same {
|
||||||
// really easy to update password once ye know how
|
// really easy to update password once ye know how
|
||||||
|
|
||||||
let tmp = PasswordModify {
|
let tmp = PasswordModify {
|
||||||
// none as we are staying on the same connection
|
// none as we are staying on the same connection
|
||||||
user_id: None,
|
user_id: None,
|
||||||
|
old_pass: Some(&pass),
|
||||||
old_pass: Some(&pass_old),
|
|
||||||
new_pass: Some(&pass_new),
|
new_pass: Some(&pass_new),
|
||||||
};
|
};
|
||||||
|
|
||||||
ldap.extended(tmp)?.success()?;
|
ldap.extended(tmp)?.success()?;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let result = get_result(&mut ldap, &dn);
|
||||||
|
|
||||||
ldap.unbind()?;
|
ldap.unbind()?;
|
||||||
|
|
||||||
Ok(json!({"result": "success"}).into())
|
Ok(json!({"result": "success", "success": result}).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_result(ldap: &mut LdapConn, dn: &str) -> ModifyResult {
|
||||||
|
let mut result = ModifyResult {
|
||||||
|
mail: None,
|
||||||
|
ssh_public_key: None,
|
||||||
|
cn: None,
|
||||||
|
sk_discord: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(temp) = ldap.search(dn, Scope::Base, "(objectClass=*)", vec!["mail", "sshPublicKey", "cn", "skDiscord"]) {
|
||||||
|
if let Ok((rs, _res)) = temp.success() {
|
||||||
|
if !rs.is_empty() {
|
||||||
|
let tmp = SearchEntry::construct(rs[0].clone());
|
||||||
|
result.mail = get_result_values(&tmp.attrs, "mail");
|
||||||
|
result.ssh_public_key = get_result_values(&tmp.attrs, "sshPublicKey");
|
||||||
|
result.cn = get_result_values(&tmp.attrs, "cn");
|
||||||
|
result.sk_discord = get_result_values(&tmp.attrs, "skDiscord");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_result_values(attrs: &HashMap<String, Vec<String>>, field: &str) -> Option<String> {
|
||||||
|
if let Some(field) = attrs.get(field) {
|
||||||
|
if !field.is_empty() {
|
||||||
|
return Some(field[0].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue