init
This commit is contained in:
		
						commit
						7e7359cd09
					
				
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
target
 | 
			
		||||
Cargo.lock
 | 
			
		||||
**/.*.sw*
 | 
			
		||||
							
								
								
									
										15
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "yunohome"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
futures = "0.3"
 | 
			
		||||
tower = "0.4"
 | 
			
		||||
tokio = { version = "1", features = [ "full" ] }
 | 
			
		||||
hyper = { version = "0.14", features = [ "full" ] }
 | 
			
		||||
axum = { version = "0.6", features = [ "headers", "http2", "macros", "tracing" ] }
 | 
			
		||||
clap = { version = "4.3", features = [ "derive" ] }
 | 
			
		||||
snafu = "0.7"
 | 
			
		||||
							
								
								
									
										11
									
								
								src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/cli.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
/// The main SSOWat program
 | 
			
		||||
#[derive(Debug, Parser)]
 | 
			
		||||
#[command(author, version, about, long_about = None)]
 | 
			
		||||
pub struct Cli {
 | 
			
		||||
    /// Where to place the UNIX socket for SSOWat
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/error.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
use snafu::Snafu;
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Snafu)]
 | 
			
		||||
#[snafu(visibility(pub))]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    #[snafu(display("Failed to spawn unix socket at {}", path.display()))]
 | 
			
		||||
    SocketCreate { path: PathBuf, source: std::io::Error },
 | 
			
		||||
 | 
			
		||||
    #[snafu(display("Failed to spawn a web server"))]
 | 
			
		||||
    Server { source: hyper::Error },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    routing::get,
 | 
			
		||||
    Router,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod cli;
 | 
			
		||||
mod error;
 | 
			
		||||
mod webserver;
 | 
			
		||||
use webserver::{handler, serve_socket};
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() -> Result<(), error::Error> {
 | 
			
		||||
    // env_logger::init();
 | 
			
		||||
 | 
			
		||||
    let args = cli::Cli::parse();
 | 
			
		||||
    let path = args.path.clone();
 | 
			
		||||
 | 
			
		||||
    let _ = tokio::fs::remove_file(&path).await;
 | 
			
		||||
    tokio::fs::create_dir_all(path.parent().unwrap())
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
 | 
			
		||||
    let app = Router::new().route("/", get(handler));
 | 
			
		||||
    serve_socket(&path, app).await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/webserver/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/webserver/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
use axum::Router;
 | 
			
		||||
use axum::extract::connect_info::ConnectInfo;
 | 
			
		||||
use snafu::prelude::*;
 | 
			
		||||
use tokio::net::UnixListener;
 | 
			
		||||
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use crate::error::*;
 | 
			
		||||
mod utils;
 | 
			
		||||
pub use utils::*;
 | 
			
		||||
 | 
			
		||||
pub async fn serve_socket(path: &Path, app: Router) -> Result<(), Error> {
 | 
			
		||||
    let uds = UnixListener::bind(path.clone())
 | 
			
		||||
        .context(SocketCreateSnafu { path: path.clone() })?;
 | 
			
		||||
    hyper::Server::builder(ServerAccept::new(uds))
 | 
			
		||||
        .serve(app.into_make_service_with_connect_info::<UdsConnectInfo>())
 | 
			
		||||
        .await.context(ServerSnafu)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn handler(ConnectInfo(info): ConnectInfo<UdsConnectInfo>) -> &'static str {
 | 
			
		||||
    println!("new connection from `{:?}`", info);
 | 
			
		||||
 | 
			
		||||
    "Hello, World!"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								src/webserver/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/webserver/utils.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,106 @@
 | 
			
		||||
use axum::extract::connect_info;
 | 
			
		||||
use futures::ready;
 | 
			
		||||
use hyper::{
 | 
			
		||||
    client::connect::{Connected, Connection},
 | 
			
		||||
    server::accept::Accept,
 | 
			
		||||
};
 | 
			
		||||
use std::{
 | 
			
		||||
    io,
 | 
			
		||||
    // path::PathBuf,
 | 
			
		||||
    pin::Pin,
 | 
			
		||||
    sync::Arc,
 | 
			
		||||
    task::{Context, Poll},
 | 
			
		||||
};
 | 
			
		||||
use tokio::{
 | 
			
		||||
    io::{AsyncRead, AsyncWrite},
 | 
			
		||||
    net::{unix::UCred, UnixListener, UnixStream},
 | 
			
		||||
};
 | 
			
		||||
use tower::BoxError;
 | 
			
		||||
 | 
			
		||||
pub struct ServerAccept {
 | 
			
		||||
    uds: UnixListener,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServerAccept {
 | 
			
		||||
    pub fn new(uds: UnixListener) -> ServerAccept {
 | 
			
		||||
        ServerAccept {
 | 
			
		||||
            uds,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Accept for ServerAccept {
 | 
			
		||||
    type Conn = UnixStream;
 | 
			
		||||
    type Error = BoxError;
 | 
			
		||||
 | 
			
		||||
    fn poll_accept(
 | 
			
		||||
        self: Pin<&mut Self>,
 | 
			
		||||
        cx: &mut Context<'_>,
 | 
			
		||||
    ) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
 | 
			
		||||
        let (stream, _addr) = ready!(self.uds.poll_accept(cx))?;
 | 
			
		||||
        Poll::Ready(Some(Ok(stream)))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ClientConnection {
 | 
			
		||||
    stream: UnixStream,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AsyncWrite for ClientConnection {
 | 
			
		||||
    fn poll_write(
 | 
			
		||||
        mut self: Pin<&mut Self>,
 | 
			
		||||
        cx: &mut Context<'_>,
 | 
			
		||||
        buf: &[u8],
 | 
			
		||||
    ) -> Poll<Result<usize, io::Error>> {
 | 
			
		||||
        Pin::new(&mut self.stream).poll_write(cx, buf)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn poll_flush(
 | 
			
		||||
        mut self: Pin<&mut Self>,
 | 
			
		||||
        cx: &mut Context<'_>,
 | 
			
		||||
    ) -> Poll<Result<(), io::Error>> {
 | 
			
		||||
        Pin::new(&mut self.stream).poll_flush(cx)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn poll_shutdown(
 | 
			
		||||
        mut self: Pin<&mut Self>,
 | 
			
		||||
        cx: &mut Context<'_>,
 | 
			
		||||
    ) -> Poll<Result<(), io::Error>> {
 | 
			
		||||
        Pin::new(&mut self.stream).poll_shutdown(cx)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AsyncRead for ClientConnection {
 | 
			
		||||
    fn poll_read(
 | 
			
		||||
        mut self: Pin<&mut Self>,
 | 
			
		||||
        cx: &mut Context<'_>,
 | 
			
		||||
        buf: &mut tokio::io::ReadBuf<'_>,
 | 
			
		||||
    ) -> Poll<io::Result<()>> {
 | 
			
		||||
        Pin::new(&mut self.stream).poll_read(cx, buf)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Connection for ClientConnection {
 | 
			
		||||
    fn connected(&self) -> Connected {
 | 
			
		||||
        Connected::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct UdsConnectInfo {
 | 
			
		||||
    peer_addr: Arc<tokio::net::unix::SocketAddr>,
 | 
			
		||||
    peer_cred: UCred,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl connect_info::Connected<&UnixStream> for UdsConnectInfo {
 | 
			
		||||
    fn connect_info(target: &UnixStream) -> Self {
 | 
			
		||||
        let peer_addr = target.peer_addr().unwrap();
 | 
			
		||||
        let peer_cred = target.peer_cred().unwrap();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            peer_addr: Arc::new(peer_addr),
 | 
			
		||||
            peer_cred,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								yunohost-api/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								yunohost-api/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "yunohost-api"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
ldap3 = "0.11"
 | 
			
		||||
env_logger = "0.10"
 | 
			
		||||
log = "0.4"
 | 
			
		||||
tokio = "1"
 | 
			
		||||
snafu = "0.7"
 | 
			
		||||
serde = { version = "1", features = [ "derive" ] }
 | 
			
		||||
serde_json = "1"
 | 
			
		||||
regex = "1.9"
 | 
			
		||||
url = "2.4"
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
scan-rules = "0.2"
 | 
			
		||||
tokio = { version = "1", features = [ "sync", "rt" ] }
 | 
			
		||||
							
								
								
									
										39
									
								
								yunohost-api/examples/info.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								yunohost-api/examples/info.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
use yunohost_api::{YunohostUsers, Username};
 | 
			
		||||
 | 
			
		||||
use std::io::{BufRead, Stdin, stdin};
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
fn acquire_loop<T: FromStr>(input: &Stdin, message: &str) -> T
 | 
			
		||||
where <T as FromStr>::Err: std::fmt::Display {
 | 
			
		||||
    loop {
 | 
			
		||||
        println!("{message}");
 | 
			
		||||
        let value = input.lock().lines().next().unwrap().unwrap();
 | 
			
		||||
        match T::from_str(&value) {
 | 
			
		||||
            Ok(v) => {
 | 
			
		||||
                return v;
 | 
			
		||||
            }, Err(e) => {
 | 
			
		||||
                println!("{}", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main(flavor = "current_thread")]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
 | 
			
		||||
    let input = stdin();
 | 
			
		||||
    let users = YunohostUsers::new(500).await.unwrap();
 | 
			
		||||
 | 
			
		||||
    loop {
 | 
			
		||||
        println!("Welcome to Yunohost! What user would you like to know about?");
 | 
			
		||||
 | 
			
		||||
        let username: Username = acquire_loop(&input, "Username:");
 | 
			
		||||
 | 
			
		||||
        if users.get_user(&username).await.unwrap() {
 | 
			
		||||
            println!("User found!");
 | 
			
		||||
        } else {
 | 
			
		||||
            println!("User not found!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								yunohost-api/examples/login.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								yunohost-api/examples/login.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
use yunohost_api::{YunohostUsers, Username, Password};
 | 
			
		||||
 | 
			
		||||
use std::io::{BufRead, Stdin, stdin};
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
fn acquire_loop<T: FromStr>(input: &Stdin, message: &str) -> T
 | 
			
		||||
where <T as FromStr>::Err: std::fmt::Display {
 | 
			
		||||
    loop {
 | 
			
		||||
        println!("{message}");
 | 
			
		||||
        let value = input.lock().lines().next().unwrap().unwrap();
 | 
			
		||||
        match T::from_str(&value) {
 | 
			
		||||
            Ok(v) => {
 | 
			
		||||
                return v;
 | 
			
		||||
            }, Err(e) => {
 | 
			
		||||
                println!("{}", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main(flavor = "current_thread")]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
 | 
			
		||||
    let input = stdin();
 | 
			
		||||
    let users = YunohostUsers::new(500).await.unwrap();
 | 
			
		||||
 | 
			
		||||
    loop {
 | 
			
		||||
        println!("Welcome to Yunohost! Please login to continue...");
 | 
			
		||||
 | 
			
		||||
        let username: Username = acquire_loop(&input, "Username:");
 | 
			
		||||
        let password: Password = acquire_loop(&input, "Password:");
 | 
			
		||||
 | 
			
		||||
        if users.check_credentials(&username, &password).await.unwrap() {
 | 
			
		||||
            println!("Successful login");
 | 
			
		||||
        } else {
 | 
			
		||||
            println!("Failed login");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								yunohost-api/src/cache.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										91
									
								
								yunohost-api/src/cache.rs
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,91 @@
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use snafu::prelude::*;
 | 
			
		||||
 | 
			
		||||
use std::os::unix::fs::MetadataExt;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::sync::RwLock;
 | 
			
		||||
 | 
			
		||||
use crate::error::*;
 | 
			
		||||
 | 
			
		||||
// TODO: Can we return reference to the inner data without cloning?
 | 
			
		||||
// Cloning is MUCH cheaper than reading from disk so it's not really a problem
 | 
			
		||||
// TODO: Should we TryFrom<Vec<u8>> so we support more than JSON/FromStr?
 | 
			
		||||
pub trait Cachable: Clone + std::fmt::Debug + for <'a> Deserialize<'a> + Default {}
 | 
			
		||||
 | 
			
		||||
impl<T: Clone + std::fmt::Debug + for<'a> Deserialize<'a> + Default> Cachable for T {}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct JsonCacheInner<T: Cachable> {
 | 
			
		||||
    mtime: i64,
 | 
			
		||||
    content: T,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T: Cachable> JsonCacheInner<T> {
 | 
			
		||||
    pub fn new() -> JsonCacheInner<T> {
 | 
			
		||||
        JsonCacheInner {
 | 
			
		||||
            mtime: i64::MIN,
 | 
			
		||||
            content: T::default(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self) -> T {
 | 
			
		||||
        self.content.clone()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct JsonCache<T: Cachable> {
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    inner: RwLock<JsonCacheInner<T>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T: Cachable> JsonCache<T> {
 | 
			
		||||
    /// Prepares a cached JSON file. Does not load from disk.
 | 
			
		||||
    pub fn new(path: &Path) -> JsonCache<T> {
 | 
			
		||||
        JsonCache {
 | 
			
		||||
            path: path.to_path_buf(),
 | 
			
		||||
            inner: RwLock::new(JsonCacheInner::new()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Loads a cached JSON file from disk to memory
 | 
			
		||||
    pub fn load(path: &Path) -> Result<JsonCache<T>, Error> {
 | 
			
		||||
        let cache = Self::new(path);
 | 
			
		||||
        cache.reload(0)?;
 | 
			
		||||
        Ok(cache)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self) -> Result<T, Error> {
 | 
			
		||||
        if let Some(new_mtime) = self.stale()? {
 | 
			
		||||
            self.reload(new_mtime)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let lock = self.inner.read().unwrap();
 | 
			
		||||
        let content = lock.get();
 | 
			
		||||
        
 | 
			
		||||
        Ok(content)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn stale(&self) -> Result<Option<i64>, Error> {
 | 
			
		||||
        let metadata = self.path.metadata().context(ReadFileSnafu { path: self.path.clone() })?;
 | 
			
		||||
        let inner = self.inner.read().unwrap();
 | 
			
		||||
        let mtime = metadata.mtime();
 | 
			
		||||
        if mtime != inner.mtime {
 | 
			
		||||
            // Needs reloading
 | 
			
		||||
            Ok(Some(mtime))
 | 
			
		||||
        } else {
 | 
			
		||||
            // Data still valid
 | 
			
		||||
            Ok(None)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn reload(&self, mtime: i64) -> Result<(), Error> {
 | 
			
		||||
        let mut inner = self.inner.write().unwrap();
 | 
			
		||||
        let content = std::fs::read_to_string(&self.path).context(ReadFileSnafu { path: self.path.clone() })?;
 | 
			
		||||
        //let content = T::from_str(&content)?;
 | 
			
		||||
        let content: T = serde_json::from_str(&content).context(InvalidJsonSnafu { path: self.path.clone() })?;
 | 
			
		||||
        inner.content = content;
 | 
			
		||||
        inner.mtime = mtime;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								yunohost-api/src/credentials.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								yunohost-api/src/credentials.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
			
		||||
use ldap3::dn_escape;
 | 
			
		||||
use serde::{Serialize, Deserialize};
 | 
			
		||||
use snafu::OptionExt;
 | 
			
		||||
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
use crate::error::*;
 | 
			
		||||
 | 
			
		||||
fn non_empty_string(s: &str) -> Option<String> {
 | 
			
		||||
    if s.trim() == "" {
 | 
			
		||||
        None
 | 
			
		||||
    } else {
 | 
			
		||||
        Some(s.to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 | 
			
		||||
pub struct Username(String);
 | 
			
		||||
 | 
			
		||||
impl Username {
 | 
			
		||||
    pub fn new(s: &str) -> Result<Username, Error> {
 | 
			
		||||
        non_empty_string(s).context(EmptyUsernameSnafu)
 | 
			
		||||
            .map(|s| Username(s))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        self.0.as_str()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn ldap_escape(&self) -> String {
 | 
			
		||||
        dn_escape(self.as_str()).to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_dn(&self, domains: &[&str]) -> String {
 | 
			
		||||
        let mut dn = format!("uid={},ou=users", self.ldap_escape());
 | 
			
		||||
        for domain in domains {
 | 
			
		||||
            dn.push_str(",dc=");
 | 
			
		||||
            dn.push_str(domain);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        dn
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromStr for Username {
 | 
			
		||||
    type Err = Error;
 | 
			
		||||
 | 
			
		||||
    fn from_str(s: &str) -> Result<Username, Error> {
 | 
			
		||||
        Username::new(s)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct Password(String);
 | 
			
		||||
 | 
			
		||||
impl Password {
 | 
			
		||||
    pub fn new(s: &str) -> Result<Password, Error> {
 | 
			
		||||
        non_empty_string(s).context(EmptyPasswordSnafu)
 | 
			
		||||
            .map(|s| Password(s))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        self.0.as_str()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn ldap_escape(&self) -> String {
 | 
			
		||||
        dn_escape(self.as_str()).to_string()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FromStr for Password {
 | 
			
		||||
    type Err = Error;
 | 
			
		||||
 | 
			
		||||
    fn from_str(s: &str) -> Result<Password, Error> {
 | 
			
		||||
        Password::new(s)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								yunohost-api/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								yunohost-api/src/error.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
use snafu::Snafu;
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use crate::Username;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Snafu)]
 | 
			
		||||
#[snafu(visibility(pub))]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    #[snafu(display("Failed to establish connection to the LDAP database at path {uri}"))]
 | 
			
		||||
    LdapInit { uri: String, source: ldap3::result::LdapError },
 | 
			
		||||
    
 | 
			
		||||
    #[snafu(display("Failed to bind on the LDAP database"))]
 | 
			
		||||
    LdapBind { source: ldap3::result::LdapError },
 | 
			
		||||
 | 
			
		||||
    #[snafu(display("Failed to search the LDAP database"))]
 | 
			
		||||
    LdapSearch { source: ldap3::result::LdapError },
 | 
			
		||||
 | 
			
		||||
    #[snafu(display("No such user: {}", username.as_str()))]
 | 
			
		||||
    LdapNoSuchUser { username: Username },
 | 
			
		||||
 | 
			
		||||
    #[snafu(display("Empty username provided for login"))]
 | 
			
		||||
    EmptyUsername,
 | 
			
		||||
 | 
			
		||||
    #[snafu(display("Empty password provided for login"))]
 | 
			
		||||
    EmptyPassword,
 | 
			
		||||
 | 
			
		||||
    #[snafu(display("Failed to read file {}", path.display()))]
 | 
			
		||||
    ReadFile { path: PathBuf, source: std::io::Error },
 | 
			
		||||
    
 | 
			
		||||
    #[snafu(display("Invalid JSON in file {}", path.display()))]
 | 
			
		||||
    InvalidJson { path: PathBuf, source: serde_json::Error },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								yunohost-api/src/helpers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								yunohost-api/src/helpers.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
// /// Remove the leading http(s) part of a URI.
 | 
			
		||||
// /// ```
 | 
			
		||||
// /// # use yunohost_api::SSOWatConfig;
 | 
			
		||||
// /// let stripped = SSOWatConfig::strip_url_protocol("http://theanarchistlibrary.org/");
 | 
			
		||||
// /// # assert_eq!("theanarchistlibrary.org/", stripped)
 | 
			
		||||
// /// ```
 | 
			
		||||
// /// Result: theanarchistlibrary.org/
 | 
			
		||||
// /// ```
 | 
			
		||||
// /// # use yunohost_api::SSOWatConfig;
 | 
			
		||||
// /// let stripped = SSOWatConfig::strip_url_protocol("misformedhttpdomain/");
 | 
			
		||||
// /// # assert_eq!("misformedhttpdomain/", stripped);
 | 
			
		||||
// /// ```
 | 
			
		||||
// /// Result: misformedhttpdomain/
 | 
			
		||||
// pub fn strip_url_protocol<'a>(uri: &'a str) -> &'a str {
 | 
			
		||||
//     let s = uri.strip_prefix("http").unwrap_or(&uri); 
 | 
			
		||||
//     let s = s.strip_prefix("s").unwrap_or(&s);
 | 
			
		||||
//     let s = s.strip_prefix("://").unwrap_or(&s);
 | 
			
		||||
//     s
 | 
			
		||||
//     // uri
 | 
			
		||||
//     //     .strip_prefix("http")
 | 
			
		||||
//     //     .and_then(|s| s.strip_prefix(""))
 | 
			
		||||
//     //     .trim_start_matches("http")
 | 
			
		||||
//     //     .trim_start_matches("s")
 | 
			
		||||
//         // .trim_start_matches("://")
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
/// Extracts the domain part of a http(s) URI.
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use yunohost_api::SSOWatConfig;
 | 
			
		||||
/// let domain = SSOWatConfig::extract_domain("https://mediaslibres.org/spip.php?page=sedna-rss");
 | 
			
		||||
/// assert_eq!("mediaslibres.org", domain);
 | 
			
		||||
/// ```
 | 
			
		||||
/// Result: mediaslibres.org
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use yunohost_api::SSOWatConfig;
 | 
			
		||||
/// let domain = SSOWatConfig::extract_domain("http://foo.bar.example.com");
 | 
			
		||||
/// assert_eq!("foo.bar.example.com", domain);
 | 
			
		||||
/// ```
 | 
			
		||||
/// Result: foo.bar.example.com
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use yunohost_api::SSOWatConfig;
 | 
			
		||||
/// let domain = SSOWatConfig::extract_domain("http://foo.bar.example.com/bar/baz");
 | 
			
		||||
/// assert_eq!("foo.bar.example.com", domain);
 | 
			
		||||
/// ```
 | 
			
		||||
/// Result: foo.bar.example.com
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use yunohost_api::SSOWatConfig;
 | 
			
		||||
/// let domain = SSOWatConfig::extract_domain("misformedhttpdomain");
 | 
			
		||||
/// # assert_eq!("misformedhttpdomain", domain)
 | 
			
		||||
/// ```
 | 
			
		||||
/// Result: misformedhttpdomain
 | 
			
		||||
pub fn extract_domain<'a>(uri: &'a str) -> &'a str {
 | 
			
		||||
    let mut split = Self::strip_url_protocol(uri).split('/');
 | 
			
		||||
    // Even if nothing was matched, there should always be one split part
 | 
			
		||||
    // If there was an additional slash, we just extracted the domain
 | 
			
		||||
    split.next().unwrap()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								yunohost-api/src/ldap.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								yunohost-api/src/ldap.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,110 @@
 | 
			
		||||
use ldap3::{LdapConnAsync, LdapConnSettings, Ldap, Scope, exop::WhoAmI, SearchEntry};
 | 
			
		||||
use snafu::prelude::*;
 | 
			
		||||
use tokio::sync::RwLock;
 | 
			
		||||
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
use crate::credentials::{Username, Password};
 | 
			
		||||
use crate::error::*;
 | 
			
		||||
 | 
			
		||||
const LDAP_PATH: &'static str = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi";
 | 
			
		||||
 | 
			
		||||
/// Opens a new LDAP connection. Does not guarantee it will stay alive
 | 
			
		||||
async fn new_ldap(timeout: Duration) -> Result<Ldap, Error> {
 | 
			
		||||
    let settings = LdapConnSettings::new().set_conn_timeout(timeout);
 | 
			
		||||
    log::info!("Opening new LDAP connection with timeout: {}ms", timeout.as_millis());
 | 
			
		||||
 | 
			
		||||
    let (conn, ldap) = LdapConnAsync::with_settings(
 | 
			
		||||
        settings,
 | 
			
		||||
        LDAP_PATH
 | 
			
		||||
    ).await.context(LdapInitSnafu { uri: LDAP_PATH.to_string() })?;
 | 
			
		||||
 | 
			
		||||
    tokio::spawn(async move {
 | 
			
		||||
        if let Err(e) = conn.drive().await {
 | 
			
		||||
            log::error!("{}", e);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    Ok(ldap)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct YunohostUsers {
 | 
			
		||||
    timeout: Duration,
 | 
			
		||||
    inner: Arc<RwLock<Ldap>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl YunohostUsers {
 | 
			
		||||
    /// Open a new connection to Yunohost LDAP database with a specific timeout in milliseconds
 | 
			
		||||
    pub async fn new(timeout: u64) -> Result<YunohostUsers, Error> {
 | 
			
		||||
        let timeout = Duration::from_millis(timeout);
 | 
			
		||||
        Ok(YunohostUsers {
 | 
			
		||||
            timeout: timeout.clone(),
 | 
			
		||||
            inner: Arc::new(RwLock::new(
 | 
			
		||||
                new_ldap(timeout).await?
 | 
			
		||||
            )),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn keepalive(&self) -> Result<(), Error> {
 | 
			
		||||
        {
 | 
			
		||||
            let mut ldap = self.inner.write().await;
 | 
			
		||||
            if ! ldap.is_closed() {
 | 
			
		||||
                // check connection is still alive with WhoAmI
 | 
			
		||||
                ldap.with_timeout(self.timeout.clone());
 | 
			
		||||
                if ldap.extended(WhoAmI).await.is_ok() {
 | 
			
		||||
                    return Ok(());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        log::warn!("LDAP connection has been closed. Opening again.");
 | 
			
		||||
        let mut ldap = self.inner.clone().write_owned().await;
 | 
			
		||||
        *ldap = new_ldap(self.timeout.clone()).await?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn check_credentials(&self, username: &Username, password: &Password) -> Result<bool, Error> {
 | 
			
		||||
        self.keepalive().await?;
 | 
			
		||||
        let mut ldap = self.inner.write().await;
 | 
			
		||||
 | 
			
		||||
        let username = username.to_dn(& [ "yunohost", "org" ]);
 | 
			
		||||
        let password = password.ldap_escape();
 | 
			
		||||
 | 
			
		||||
        log::debug!("Attempting LDAP login for {}", &username);
 | 
			
		||||
        ldap.with_timeout(self.timeout.clone());
 | 
			
		||||
        let reply = ldap.simple_bind(
 | 
			
		||||
            &username,
 | 
			
		||||
            &password,
 | 
			
		||||
        ).await.context(LdapBindSnafu)?;
 | 
			
		||||
        log::debug!("{:#?}", reply);
 | 
			
		||||
 | 
			
		||||
        // Successful auth if return code is 0
 | 
			
		||||
        Ok(reply.rc == 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_user(&self, username: &Username) -> Result<bool, Error> {
 | 
			
		||||
        self.keepalive().await?;
 | 
			
		||||
        let mut ldap = self.inner.write().await;
 | 
			
		||||
        ldap.with_timeout(self.timeout.clone());
 | 
			
		||||
 | 
			
		||||
        let res = ldap.search(
 | 
			
		||||
            &username.to_dn(& [ "yunohost", "org" ]),
 | 
			
		||||
            Scope::Base,
 | 
			
		||||
            "(objectclass=*)",
 | 
			
		||||
            [ "+" ],
 | 
			
		||||
        ).await.context(LdapSearchSnafu)?;
 | 
			
		||||
 | 
			
		||||
        if let Ok((res, _)) = res.success() {
 | 
			
		||||
            // There should be only one user with this uid
 | 
			
		||||
            let res = res.into_iter().take(1).next().unwrap();
 | 
			
		||||
            let res = SearchEntry::construct(res);
 | 
			
		||||
            log::debug!("Found:\n{:#?}", res);
 | 
			
		||||
        } else {
 | 
			
		||||
            return Err(Error::LdapNoSuchUser { username: username.clone() });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								yunohost-api/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								yunohost-api/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
mod cache;
 | 
			
		||||
pub use cache::JsonCache;
 | 
			
		||||
mod credentials;
 | 
			
		||||
pub use credentials::{Username, Password};
 | 
			
		||||
mod error;
 | 
			
		||||
pub use error::Error;
 | 
			
		||||
mod ldap;
 | 
			
		||||
pub use ldap::YunohostUsers;
 | 
			
		||||
mod permissions;
 | 
			
		||||
pub use permissions::{YunohostPermissions, SSOWatConfig, PermissionName};
 | 
			
		||||
							
								
								
									
										30
									
								
								yunohost-api/src/permissions/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								yunohost-api/src/permissions/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use crate::error::*;
 | 
			
		||||
use crate::JsonCache;
 | 
			
		||||
 | 
			
		||||
mod ssowat;
 | 
			
		||||
pub use ssowat::{SSOWatConfig, PermissionName};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct YunohostPermissions {
 | 
			
		||||
    ssowat: JsonCache<SSOWatConfig>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl YunohostPermissions {
 | 
			
		||||
    pub fn new() -> Result<YunohostPermissions, Error> {
 | 
			
		||||
        Self::from_path("/etc/ssowat/conf.json")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_path<T: AsRef<Path>>(path: T) -> Result<YunohostPermissions, Error> {
 | 
			
		||||
        let path = path.as_ref();
 | 
			
		||||
        let ssowat: JsonCache<SSOWatConfig> = JsonCache::load(path)?;
 | 
			
		||||
        Ok(YunohostPermissions {
 | 
			
		||||
            ssowat,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn ssowat_config(&self) -> Result<SSOWatConfig, Error> {
 | 
			
		||||
        self.ssowat.get()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								yunohost-api/src/permissions/ssowat.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								yunohost-api/src/permissions/ssowat.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
			
		||||
use serde::{Serialize, Deserialize};
 | 
			
		||||
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use crate::{Username};
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
 | 
			
		||||
pub struct SSOWatConfig {
 | 
			
		||||
    domains: Vec<String>,
 | 
			
		||||
    permissions: HashMap<PermissionName, Permission>,
 | 
			
		||||
    portal_domain: String,
 | 
			
		||||
    portal_path: String,
 | 
			
		||||
    redirected_urls: HashMap<String, String>,
 | 
			
		||||
    theme: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl SSOWatConfig {
 | 
			
		||||
    pub fn permission_for_uri(&self, uri: &Url) -> Option<PermissionName> {
 | 
			
		||||
        // First check if the domain is actually managed by SSOWat
 | 
			
		||||
        if let Some(domain) = uri.domain() {
 | 
			
		||||
            if ! self.domains.contains(&domain.to_string()) {
 | 
			
		||||
                // Domain not managed
 | 
			
		||||
                return None;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // No domain (eg. http://8.8.8.8/)
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
        let domain = Self::extract_domain(uri);
 | 
			
		||||
 | 
			
		||||
        if self.domains.contains(&domain.to_string()) {
 | 
			
		||||
            todo!();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        todo!();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
 | 
			
		||||
pub struct Permission {
 | 
			
		||||
    auth_header: bool,
 | 
			
		||||
    label: String,
 | 
			
		||||
    public: bool,
 | 
			
		||||
    show_tile: bool,
 | 
			
		||||
    uris: Vec<String>,
 | 
			
		||||
    use_remote_user_in_nginx_conf: bool,
 | 
			
		||||
    users: Vec<Username>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
 | 
			
		||||
pub struct PermissionName {
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    name: String,
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user