Compare commits
14 changed files with 67 additions and 189 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -148,12 +148,6 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.23.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.1"
|
version = "2.13.1"
|
||||||
|
|
@ -518,7 +512,7 @@ version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "105256b138a7ed84ac1ae375870eae23be5f8fed16ca172ae5f1c1aa37bd4242"
|
checksum = "105256b138a7ed84ac1ae375870eae23be5f8fed16ca172ae5f1c1aa37bd4242"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"ldap3_lber",
|
"ldap3_lber",
|
||||||
"nom",
|
"nom",
|
||||||
|
|
@ -543,7 +537,6 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64 0.23.1",
|
|
||||||
"camino",
|
"camino",
|
||||||
"dn_escape",
|
"dn_escape",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ argh = "0.1.19"
|
||||||
async-trait = "0.1.92"
|
async-trait = "0.1.92"
|
||||||
axum = { version = "0.8.9", optional = true, features = ["macros"] }
|
axum = { version = "0.8.9", optional = true, features = ["macros"] }
|
||||||
axum-extra = { version = "0.12.6", features = ["cookie"], optional = true }
|
axum-extra = { version = "0.12.6", features = ["cookie"], optional = true }
|
||||||
base64 = "0.23.1"
|
|
||||||
camino = { version = "1.2.5", features = ["serde1"] }
|
camino = { version = "1.2.5", features = ["serde1"] }
|
||||||
dn_escape = { path = "vendor/dn_escape" }
|
dn_escape = { path = "vendor/dn_escape" }
|
||||||
futures-util = { version = "0.3.34", features = ["sink"] }
|
futures-util = { version = "0.3.34", features = ["sink"] }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Error as JsonError;
|
use serde_json::Error as JsonError;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
@ -34,7 +35,7 @@ impl fmt::Display for FilesystemDatabaseError {
|
||||||
|
|
||||||
impl std::error::Error for FilesystemDatabaseError {}
|
impl std::error::Error for FilesystemDatabaseError {}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct FilesystemDatabase {
|
pub struct FilesystemDatabase {
|
||||||
// TODO: Once we have common validation steps in place across DB backends, we can reduce cloning.
|
// TODO: Once we have common validation steps in place across DB backends, we can reduce cloning.
|
||||||
// For now, we clone the DB on every write operation, and update it when saving to disk
|
// For now, we clone the DB on every write operation, and update it when saving to disk
|
||||||
|
|
@ -58,14 +59,10 @@ impl FilesystemDatabase {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Initializing empty database in file {path}. Checking permissions...");
|
db.save().await?;
|
||||||
db.save_self().await?;
|
|
||||||
tracing::info!("Database successfully created");
|
|
||||||
|
|
||||||
return Ok(Database::new(db));
|
return Ok(Database::new(db));
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Loading database from file {path}");
|
|
||||||
let s = tokio::fs::read(path)
|
let s = tokio::fs::read(path)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Box::new(FilesystemDatabaseError::ReadFileIO(path.to_path_buf(), e)))?;
|
.map_err(|e| Box::new(FilesystemDatabaseError::ReadFileIO(path.to_path_buf(), e)))?;
|
||||||
|
|
@ -77,29 +74,13 @@ impl FilesystemDatabase {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to save a new state of the DB, effectively switching to the new state
|
pub async fn save(&mut self) -> Result<(), BoxedError> {
|
||||||
/// only if the save is successful.
|
let s = serde_json::to_string(&self)
|
||||||
pub async fn save(&mut self, new_db: MemoryDatabase) -> Result<(), BoxedError> {
|
|
||||||
self.save_inner(&new_db).await?;
|
|
||||||
self.inner = new_db;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves an in-memory DB to disk. Cannot be used directly to observe exclusive access.
|
|
||||||
async fn save_inner(&self, inner: &MemoryDatabase) -> Result<(), BoxedError> {
|
|
||||||
let s = serde_json::to_string(inner)
|
|
||||||
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileJson(self.path.clone(), e)))?;
|
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileJson(self.path.clone(), e)))?;
|
||||||
Ok(tokio::fs::write(&self.path, &s)
|
Ok(tokio::fs::write(&self.path, &s)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileIO(self.path.clone(), e)))?)
|
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileIO(self.path.clone(), e)))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save the database without switching to a new state.
|
|
||||||
///
|
|
||||||
/// Used when initializing the DB, to check for permissions.
|
|
||||||
pub async fn save_self(&mut self) -> Result<(), BoxedError> {
|
|
||||||
self.save_inner(&self.inner).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|
@ -114,7 +95,9 @@ impl DatabaseInterface for FilesystemDatabase {
|
||||||
return Ok(Err(e));
|
return Ok(Err(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save(new_db).await?;
|
self.save().await?;
|
||||||
|
self.inner = new_db;
|
||||||
|
|
||||||
Ok(Ok(()))
|
Ok(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +119,9 @@ impl DatabaseInterface for FilesystemDatabase {
|
||||||
return Ok(Err(e));
|
return Ok(Err(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save(new_db).await?;
|
self.save().await?;
|
||||||
|
self.inner = new_db;
|
||||||
|
|
||||||
Ok(Ok(()))
|
Ok(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +136,9 @@ impl DatabaseInterface for FilesystemDatabase {
|
||||||
return Ok(Err(e));
|
return Ok(Err(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save(new_db).await?;
|
self.save().await?;
|
||||||
|
self.inner = new_db;
|
||||||
|
|
||||||
Ok(Ok(()))
|
Ok(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ pub trait DatabaseInterface: std::fmt::Debug + Send + Sync + 'static {
|
||||||
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
) -> Result<Result<(), UserCreationError>, BoxedError>;
|
||||||
|
|
||||||
async fn list_all_domains(&self) -> Result<Vec<Domain>, BoxedError>;
|
async fn list_all_domains(&self) -> Result<Vec<Domain>, BoxedError>;
|
||||||
|
#[expect(unused)]
|
||||||
async fn list_all_users(&self) -> Result<Vec<User>, BoxedError>;
|
async fn list_all_users(&self) -> Result<Vec<User>, BoxedError>;
|
||||||
|
|
||||||
/// List users on a specific domain.
|
/// List users on a specific domain.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ pub struct MemoryDatabase {
|
||||||
impl MemoryDatabase {
|
impl MemoryDatabase {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new() -> Database {
|
pub fn new() -> Database {
|
||||||
tracing::warn!("Using in-memory database. Data will not be saved across restarts!");
|
|
||||||
Database::new(Self::default())
|
Database::new(Self::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use axum::extract::{Form, State};
|
use axum::extract::{Form, State};
|
||||||
use axum::response::{Html, IntoResponse, Response};
|
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||||
use axum_extra::extract::cookie::CookieJar;
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use minijinja::context;
|
use minijinja::context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::db::{DatabaseInterface, UserRef};
|
use crate::db::{DatabaseInterface, UserRef};
|
||||||
use crate::http::{HttpState, InternalRedirect, OptionalHttpSession};
|
use crate::http::{HttpState, OptionalHttpSession};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LoginForm {
|
pub struct LoginForm {
|
||||||
|
|
@ -23,13 +23,12 @@ pub enum LoginError {
|
||||||
pub async fn login_page(
|
pub async fn login_page(
|
||||||
State(state): State<HttpState>,
|
State(state): State<HttpState>,
|
||||||
login_error: Option<LoginError>,
|
login_error: Option<LoginError>,
|
||||||
redirect: InternalRedirect,
|
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let page = state
|
let page = state
|
||||||
.templates
|
.templates
|
||||||
.get_template("login.html")
|
.get_template("login.html")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.render(context! {login_error => login_error, redirect => format!("/login/?redirect={}", redirect.to_base64url())})
|
.render(context! {login_error => login_error})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
(StatusCode::OK, Html(page)).into_response()
|
(StatusCode::OK, Html(page)).into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -37,25 +36,23 @@ pub async fn login_page(
|
||||||
pub async fn get_login(
|
pub async fn get_login(
|
||||||
State(state): State<HttpState>,
|
State(state): State<HttpState>,
|
||||||
maybe_session: Option<OptionalHttpSession>,
|
maybe_session: Option<OptionalHttpSession>,
|
||||||
redirect: InternalRedirect,
|
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if maybe_session.is_some() {
|
if maybe_session.is_some() {
|
||||||
return redirect.to_redirect().into_response();
|
return Redirect::to("/").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
login_page(State(state), None, redirect).await
|
login_page(State(state), None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_login(
|
pub async fn post_login(
|
||||||
State(state): State<HttpState>,
|
State(state): State<HttpState>,
|
||||||
session: Option<OptionalHttpSession>,
|
session: Option<OptionalHttpSession>,
|
||||||
cookies: CookieJar,
|
cookies: CookieJar,
|
||||||
redirect: InternalRedirect,
|
|
||||||
Form(form): Form<LoginForm>,
|
Form(form): Form<LoginForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if session.is_some() {
|
if session.is_some() {
|
||||||
// Already logged in
|
// Already logged in
|
||||||
return (cookies, redirect.to_redirect()).into_response();
|
return (cookies, Redirect::to("/")).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let req_user = match UserRef::from_user_maybe_domain(&form.username) {
|
let req_user = match UserRef::from_user_maybe_domain(&form.username) {
|
||||||
|
|
@ -86,9 +83,9 @@ pub async fn post_login(
|
||||||
};
|
};
|
||||||
|
|
||||||
let cookies = state.sessions.add_session(user, cookies);
|
let cookies = state.sessions.add_session(user, cookies);
|
||||||
(cookies, redirect.to_redirect()).into_response()
|
(cookies, Redirect::to("/")).into_response()
|
||||||
} else {
|
} else {
|
||||||
login_page(State(state), Some(LoginError::InvalidCredentials), redirect)
|
login_page(State(state), Some(LoginError::InvalidCredentials))
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use axum::response::{IntoResponse, Redirect, Response};
|
||||||
use axum_extra::extract::cookie::CookieJar;
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
|
|
||||||
use crate::http::login::{LoginError, login_page};
|
use crate::http::login::{LoginError, login_page};
|
||||||
use crate::http::{HttpState, InternalRedirect, OptionalHttpSession};
|
use crate::http::{HttpState, OptionalHttpSession};
|
||||||
|
|
||||||
pub async fn logout(
|
pub async fn logout(
|
||||||
State(state): State<HttpState>,
|
State(state): State<HttpState>,
|
||||||
|
|
@ -17,12 +17,7 @@ pub async fn logout(
|
||||||
let cookies = state.sessions.remove_session(&session, cookies);
|
let cookies = state.sessions.remove_session(&session, cookies);
|
||||||
(
|
(
|
||||||
cookies,
|
cookies,
|
||||||
login_page(
|
login_page(State(state), Some(LoginError::SessionInvalidated)).await,
|
||||||
State(state),
|
|
||||||
Some(LoginError::SessionInvalidated),
|
|
||||||
InternalRedirect::new(),
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ mod domain;
|
||||||
mod home;
|
mod home;
|
||||||
mod login;
|
mod login;
|
||||||
mod logout;
|
mod logout;
|
||||||
mod redirect;
|
|
||||||
use redirect::InternalRedirect;
|
|
||||||
mod session;
|
mod session;
|
||||||
use session::{HttpSession, HttpSessionManager, OptionalHttpSession};
|
use session::{HttpSession, HttpSessionManager, OptionalHttpSession};
|
||||||
mod user;
|
mod user;
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
use axum::extract::{FromRequestParts, Query};
|
|
||||||
use axum::response::Redirect;
|
|
||||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE};
|
|
||||||
use http::request::Parts;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::http::HttpState;
|
|
||||||
|
|
||||||
/// Internal redirect to a different page.
|
|
||||||
///
|
|
||||||
/// Is usually constructed from a base64url-encoded `redirect`
|
|
||||||
/// query string, for example in the login page.
|
|
||||||
pub struct InternalRedirect(Option<String>);
|
|
||||||
|
|
||||||
impl InternalRedirect {
|
|
||||||
/// Creates a new redirection from a base64url-encoded string.
|
|
||||||
///
|
|
||||||
/// An invalid redirect is silently discarded and treated as no redirect.
|
|
||||||
pub fn from_base64url(s: &str) -> Self {
|
|
||||||
let s = match URL_SAFE.decode(s) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::debug!("Malformed login redirect URL, treating as empty: {e}");
|
|
||||||
return Self::new();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let s = match String::from_utf8(s) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::debug!("Malformed login redirect URL bytes, treating as empty: {e}");
|
|
||||||
return Self::new();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::from_string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turns into a base64url string that can be added to a URL.
|
|
||||||
pub fn to_base64url(&self) -> String {
|
|
||||||
let s = self
|
|
||||||
.0
|
|
||||||
.as_ref()
|
|
||||||
.expect("Cannot call InternalRedirect::to_base64url on an empty redirect");
|
|
||||||
URL_SAFE.encode(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new redirection from a raw string.
|
|
||||||
///
|
|
||||||
/// If the string is empty or seems to redirect outside of our website (absolute URL),
|
|
||||||
/// the redirect is silently discarded and treated as no redirect.
|
|
||||||
pub fn from_string(s: String) -> Self {
|
|
||||||
if s.trim().is_empty() {
|
|
||||||
return Self(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.starts_with("http://") || s.starts_with("https://") {
|
|
||||||
tracing::debug!("Invalid login redirect URL, treating as empty: {s}");
|
|
||||||
return Self(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self(Some(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_redirect(&self) -> Redirect {
|
|
||||||
if let Some(url) = &self.0 {
|
|
||||||
Redirect::to(url)
|
|
||||||
} else {
|
|
||||||
Redirect::to("/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromRequestParts<HttpState> for InternalRedirect {
|
|
||||||
type Rejection = !;
|
|
||||||
|
|
||||||
async fn from_request_parts(
|
|
||||||
parts: &mut Parts,
|
|
||||||
state: &HttpState,
|
|
||||||
) -> Result<Self, Self::Rejection> {
|
|
||||||
let Ok(f) = Query::<RedirectForm>::from_request_parts(parts, state).await else {
|
|
||||||
return Ok(Self::new());
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self::from_base64url(&f.redirect))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct RedirectForm {
|
|
||||||
redirect: String,
|
|
||||||
}
|
|
||||||
|
|
@ -2,13 +2,12 @@ use axum::extract::{FromRequestParts, OptionalFromRequestParts};
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||||
use http::request::Parts;
|
use http::request::Parts;
|
||||||
use http::uri::Uri;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::db::User;
|
use crate::db::User;
|
||||||
use crate::http::{HttpState, InternalRedirect};
|
use crate::http::HttpState;
|
||||||
|
|
||||||
pub const COOKIE_NAME: &str = "lldap_session";
|
pub const COOKIE_NAME: &str = "lldap_session";
|
||||||
|
|
||||||
|
|
@ -92,22 +91,10 @@ impl FromRequestParts<HttpState> for HttpSession {
|
||||||
state: &HttpState,
|
state: &HttpState,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
let cookies = CookieJar::from_request_parts(parts, state).await.unwrap();
|
let cookies = CookieJar::from_request_parts(parts, state).await.unwrap();
|
||||||
if let Some(session) = state.sessions.get_session(&cookies) {
|
state
|
||||||
return Ok(session);
|
.sessions
|
||||||
}
|
.get_session(&cookies)
|
||||||
|
.ok_or(Redirect::to("/login"))
|
||||||
// Extract the requested URL, turn it into base64, to let the login page
|
|
||||||
// know where to redirect us.
|
|
||||||
// But first, remove the scheme/host/port from URL.
|
|
||||||
let s = Uri::builder()
|
|
||||||
.path_and_query(parts.uri.path_and_query().unwrap().clone())
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let r = InternalRedirect::from_string(s.to_string());
|
|
||||||
Err(Redirect::to(&format!(
|
|
||||||
"/login?redirect={}",
|
|
||||||
r.to_base64url()
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ pub async fn ldap_handler(mut stream: LdapStream, mut db: Database) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return true to keep the connection going, false to close it.
|
/// Return true to keep the connection going, false to close it.
|
||||||
#[tracing::instrument(name = "ldap-handler", skip(client_state, db, stream, msg))]
|
#[tracing::instrument(name = "ldap-handler", skip(client_state, db, stream))]
|
||||||
pub async fn ldap_handler_inner(
|
pub async fn ldap_handler_inner(
|
||||||
stream: &mut LdapStream,
|
stream: &mut LdapStream,
|
||||||
msg: LdapMsg,
|
msg: LdapMsg,
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,9 @@ pub async fn search_success(
|
||||||
fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResultEntry {
|
fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResultEntry {
|
||||||
let mut res: Vec<LdapPartialAttribute> = vec![];
|
let mut res: Vec<LdapPartialAttribute> = vec![];
|
||||||
for attr in req_attrs {
|
for attr in req_attrs {
|
||||||
if let Some(attr_values) = match attr.as_str() {
|
if let Some(attr_value) = match attr.as_str() {
|
||||||
"uid" => Some(vec![user.username.clone()]),
|
"uid" => Some(user.username.clone()),
|
||||||
"cn" | "mail" => Some(vec![user.mail.clone()]),
|
"cn" | "mail" => Some(user.mail.clone()),
|
||||||
// TODO: group membership
|
|
||||||
"memberof" => Some(vec![]),
|
|
||||||
_ => {
|
_ => {
|
||||||
tracing::warn!("Ignoring unknown attr in search query: {attr}");
|
tracing::warn!("Ignoring unknown attr in search query: {attr}");
|
||||||
None
|
None
|
||||||
|
|
@ -163,8 +161,9 @@ fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResult
|
||||||
} {
|
} {
|
||||||
res.push(LdapPartialAttribute {
|
res.push(LdapPartialAttribute {
|
||||||
atype: attr.clone(),
|
atype: attr.clone(),
|
||||||
// LDAP response expects raw byte vec for each value
|
// TODO: there may be multiple values here in the future,
|
||||||
vals: attr_values.into_iter().map(Vec::from).collect(),
|
// eg. mailaliases
|
||||||
|
vals: vec![Vec::from(attr_value)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/main.rs
37
src/main.rs
|
|
@ -18,16 +18,36 @@ use ldap::ldap_handler;
|
||||||
use listener::ListenerPath;
|
use listener::ListenerPath;
|
||||||
|
|
||||||
async fn create_dummy_users(db: &mut Database) {
|
async fn create_dummy_users(db: &mut Database) {
|
||||||
// TODO: customize admin password
|
db.create_user(User {
|
||||||
if db.list_all_users().await.unwrap().is_empty() {
|
username: "admin".to_string(),
|
||||||
tracing::info!("Creating admin account with default `adminadmin` password");
|
domain: None,
|
||||||
|
password: "adminadmin".to_string(),
|
||||||
|
// TODO: what should we put here?
|
||||||
|
mail: "admin".to_string(),
|
||||||
|
role: Role::Admin,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
for letter in &["a", "b", "c"] {
|
||||||
|
let domain = format!("{letter}.localhost");
|
||||||
|
db.create_domain(&domain).await.unwrap().unwrap();
|
||||||
db.create_user(User {
|
db.create_user(User {
|
||||||
username: "admin".to_string(),
|
username: letter.to_string(),
|
||||||
domain: None,
|
domain: Some(domain.clone()),
|
||||||
password: "adminadmin".to_string(),
|
password: "adminadmin".to_string(),
|
||||||
// TODO: what should we put here?
|
mail: format!("{letter}@{domain}"),
|
||||||
mail: "admin".to_string(),
|
role: Role::DomainAdmin(domain.clone()),
|
||||||
role: Role::Admin,
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
db.create_user(User {
|
||||||
|
username: format!("user{letter}"),
|
||||||
|
domain: Some(domain.clone()),
|
||||||
|
password: "adminadmin".to_string(),
|
||||||
|
mail: format!("user{letter}@{domain}"),
|
||||||
|
role: Role::User,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -54,7 +74,6 @@ async fn main() -> Result<(), GlobalError> {
|
||||||
} else {
|
} else {
|
||||||
MemoryDatabase::new()
|
MemoryDatabase::new()
|
||||||
};
|
};
|
||||||
tracing::info!("Database loaded successfully");
|
|
||||||
create_dummy_users(&mut db).await;
|
create_dummy_users(&mut db).await;
|
||||||
|
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<p>Logout successful</p>
|
<p>Logout successful</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="{{ login_url }}">
|
<form method="POST" action="/login">
|
||||||
<div id="login-line">
|
<div id="login-line">
|
||||||
<img src="/assets/img/user.svg">
|
<img src="/assets/img/user.svg">
|
||||||
<input type="text" name="username" id="username" placeholder="Username">
|
<input type="text" name="username" id="username" placeholder="Username">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue