Compare commits

...

3 Commits

Author SHA1 Message Date
1667c3295b Start work on login form 2023-08-22 17:00:55 +02:00
4eb3ac6350 Implement Serialize, Deserialize and TryFromMultipart for Username/Password
Enable the axum feature to use it
2023-08-22 16:51:52 +02:00
2d1f3a985f Add permissions on the socket 2023-08-22 15:18:04 +02:00
12 changed files with 189 additions and 19 deletions

View File

@ -10,7 +10,7 @@ futures = "0.3"
tower = "0.4" tower = "0.4"
tokio = { version = "1", features = [ "full" ] } tokio = { version = "1", features = [ "full" ] }
hyper = { version = "0.14", features = [ "full" ] } hyper = { version = "0.14", features = [ "full" ] }
axum = { version = "0.6", features = [ "headers", "http2", "macros", "tracing" ] } axum = { version = "0.6", features = [ "headers", "http2", "macros", "tracing", "json" ] }
clap = { version = "4.3", features = [ "derive" ] } clap = { version = "4.3", features = [ "derive" ] }
snafu = "0.7" snafu = "0.7"
log = "0.4" log = "0.4"
@ -20,7 +20,8 @@ env_logger = "0.10"
ring = "0.16" ring = "0.16"
hex = "0.4" hex = "0.4"
chrono = { version = "0.4", features = [ "serde" ] } chrono = { version = "0.4", features = [ "serde" ] }
yunohost-api = { path = "yunohost-api" } yunohost-api = { path = "yunohost-api", features = [ "axum" ] }
axum_typed_multipart = "0.8" axum_typed_multipart = "0.8"
async-trait = "0.1" async-trait = "0.1"
serde = { version = "1", features = [ "derive" ] } serde = { version = "1", features = [ "derive" ] }
file-owner = { version = "0.1" }

View File

@ -16,4 +16,16 @@ pub enum Error {
#[snafu(display("{}", source))] #[snafu(display("{}", source))]
Session { source: crate::state::sessions::SessionError }, Session { source: crate::state::sessions::SessionError },
#[snafu(display("Failed to executed tokio task"))]
TokioTask { source: tokio::task::JoinError },
#[snafu(display("Failed to set permissions on file {}", path.display()))]
Permissions { path: PathBuf, source: std::io::Error },
#[snafu(display("Failed to set owner on file {}", path.display()))]
PermissionsChown { path: PathBuf, source: file_owner::FileOwnerError },
#[snafu(display("Failed to set group on file {}", path.display()))]
PermissionsChgrp { path: PathBuf, source: file_owner::FileOwnerError },
} }

View File

@ -1,5 +1,6 @@
#[macro_use] extern crate async_trait; #[macro_use] extern crate async_trait;
#[macro_use] extern crate axum; #[macro_use] extern crate axum;
#[macro_use] extern crate log;
#[macro_use] extern crate serde; #[macro_use] extern crate serde;
use clap::Parser; use clap::Parser;

View File

@ -1,16 +1,19 @@
use axum::{ use axum::{
extract::{FromRequest, Form, Json, Query}, extract::{FromRequest, Form, Json, Query, State},
http::{self, Request, StatusCode}, http::{self, Request, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
RequestExt, RequestExt,
}; };
use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
use yunohost_api::{Username, Password};
use crate::state::RoutableAppState;
#[derive(Debug, TryFromMultipart, Deserialize)] #[derive(Debug, TryFromMultipart, Deserialize)]
pub struct LoginForm { pub struct LoginForm {
username: String, username: Username,
#[allow(dead_code)] #[allow(dead_code)]
password: String, password: Password,
} }
#[async_trait] #[async_trait]
@ -52,6 +55,10 @@ where
} }
#[debug_handler] #[debug_handler]
pub async fn route(form: LoginForm) -> String { pub async fn route(state: State<RoutableAppState>, form: LoginForm) -> String {
format!("Welcome {}", form.username) if state.check_login(&form.username, &form.password).await.unwrap() {
format!("Welcome {}", &form.username)
} else {
format!("Invalid login for {}", &form.username)
}
} }

View File

@ -13,7 +13,7 @@ mod login;
pub fn router(subpath: Option<String>, state: RoutableAppState) -> Router { pub fn router(subpath: Option<String>, state: RoutableAppState) -> Router {
let app = Router::new() let app = Router::new()
.route("/", get(index::route)) .route("/", get(index::route))
.route("/login/", get(login::route)) .route("/login/", post(login::route))
.with_state(state); .with_state(state);
if let Some(p) = subpath { if let Some(p) = subpath {
Router::new() Router::new()

View File

@ -1,5 +1,5 @@
use snafu::prelude::*; use snafu::prelude::*;
use yunohost_api::YunohostUsers; use yunohost_api::{YunohostUsers, Username, Password};
use std::sync::Arc; use std::sync::Arc;
@ -23,4 +23,8 @@ impl AppState {
users: YunohostUsers::new(500).await.context(YunohostSnafu)?, users: YunohostUsers::new(500).await.context(YunohostSnafu)?,
}) })
} }
pub async fn check_login(&self, username: &Username, password: &Password) -> Result<bool, Error> {
self.users.check_credentials(username, password).await.context(YunohostSnafu)
}
} }

78
src/utils/fs.rs Normal file
View File

@ -0,0 +1,78 @@
use file_owner::PathExt;
use snafu::prelude::*;
use tokio::{
fs::set_permissions,
task::spawn_blocking,
};
use std::{
fs::Permissions,
os::unix::fs::PermissionsExt,
path::Path,
};
use crate::error::*;
pub struct FSPermissions {
pub owner: Option<String>,
pub group: Option<String>,
pub mode: Option<u32>,
}
impl FSPermissions {
pub fn new() -> FSPermissions {
FSPermissions {
owner: None,
group: None,
mode: None,
}
}
pub fn chown(mut self, owner: &str) -> Self {
self.owner = Some(owner.to_string());
self
}
pub fn chgrp(mut self, group: &str) -> Self {
self.group = Some(group.to_string());
self
}
pub fn chmod(mut self, mode: u32) -> Self {
self.mode = Some(mode);
self
}
pub async fn apply_to(&self, path: &Path) -> Result<(), Error> {
if let Some(mode) = self.mode {
set_permissions(
path,
Permissions::from_mode(mode)
).await.context(PermissionsSnafu { path: path.to_path_buf()})?;
}
if let Some(owner) = &self.owner {
let owner = owner.to_string();
let path = path.to_path_buf();
let _ = spawn_blocking(move || -> Result<(), Error> {
Ok(
path.set_owner(owner.as_str())
.context(PermissionsChownSnafu { path: path.to_path_buf() })?
)
}).await.context(TokioTaskSnafu)?;
}
if let Some(group) = &self.group {
let group = group.to_string();
let path = path.to_path_buf();
let _ = spawn_blocking(move || -> Result<(), Error> {
Ok(
path.set_group(group.as_str())
.context(PermissionsChgrpSnafu { path: path.to_path_buf() })?
)
}).await.context(TokioTaskSnafu)?;
}
Ok(())
}
}

View File

@ -1,2 +1,3 @@
pub mod fs;
pub mod time; pub mod time;
pub mod socket; pub mod socket;

View File

@ -21,7 +21,10 @@ use tokio::{
}; };
use tower::BoxError; use tower::BoxError;
use crate::error::*; use crate::{
error::*,
utils::fs::FSPermissions,
};
pub struct ServerAccept { pub struct ServerAccept {
uds: UnixListener, uds: UnixListener,
@ -118,7 +121,9 @@ pub async fn serve(path: &Path, app: Router) -> Result<(), Error> {
.await .await
.unwrap(); .unwrap();
// TODO: set permissions // TODO: make proper permissions
// Apply 777 permissions
FSPermissions::new().chmod(0o777).apply_to(&path).await?;
let uds = UnixListener::bind(path.clone()) let uds = UnixListener::bind(path.clone())
.context(SocketCreateSnafu { path: path.clone() })?; .context(SocketCreateSnafu { path: path.clone() })?;

View File

@ -15,7 +15,13 @@ serde = { version = "1", features = [ "derive" ] }
serde_json = "1" serde_json = "1"
regex = "1.9" regex = "1.9"
url = "2.4" url = "2.4"
futures-util = { version = "0.3", optional = true }
axum = { version = "0.6", optional = true }
axum_typed_multipart = { version = "0.8", optional = true }
[dev-dependencies] [dev-dependencies]
scan-rules = "0.2" scan-rules = "0.2"
tokio = { version = "1", features = [ "sync", "rt" ] } tokio = { version = "1", features = [ "sync", "rt" ] }
[features]
axum = [ "dep:futures-util", "dep:axum", "dep:axum_typed_multipart" ]

View File

@ -1,6 +1,17 @@
use ldap3::dn_escape; use ldap3::dn_escape;
use serde::{Serialize, Deserialize}; use snafu::prelude::*;
use snafu::OptionExt;
#[cfg(feature="axum")]
use axum::{
async_trait,
body::Bytes,
};
#[cfg(feature="axum")]
use axum_typed_multipart::{FieldMetadata, TryFromChunks, TypedMultipartError};
#[cfg(feature="axum")]
use futures_util::stream::Stream;
use serde::{Serialize, Deserialize, Deserializer};
use std::str::FromStr; use std::str::FromStr;
@ -14,7 +25,7 @@ fn non_empty_string(s: &str) -> Option<String> {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
pub struct Username(String); pub struct Username(String);
impl Username { impl Username {
@ -56,7 +67,29 @@ impl std::fmt::Display for Username {
} }
} }
#[derive(Clone, Debug)] impl<'de> Deserialize<'de> for Username {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(feature="axum")]
#[async_trait]
impl TryFromChunks for Username {
async fn try_from_chunks(
chunks: impl Stream<Item = Result<Bytes, TypedMultipartError>> + Send + Sync + Unpin,
metadata: FieldMetadata,
) -> Result<Self, TypedMultipartError> {
let string = String::try_from_chunks(chunks, metadata).await?;
let data = Self::from_str(&string).map_err(|e| TypedMultipartError::Other { source: e.into() })?;
Ok(data)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct Password(String); pub struct Password(String);
impl Password { impl Password {
@ -81,3 +114,25 @@ impl FromStr for Password {
Password::new(s) Password::new(s)
} }
} }
impl<'de> Deserialize<'de> for Password {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(feature="axum")]
#[async_trait]
impl TryFromChunks for Password {
async fn try_from_chunks(
chunks: impl Stream<Item = Result<Bytes, TypedMultipartError>> + Send + Sync + Unpin,
metadata: FieldMetadata,
) -> Result<Self, TypedMultipartError> {
let string = String::try_from_chunks(chunks, metadata).await?;
let data = Self::from_str(&string).map_err(|e| TypedMultipartError::Other { source: e.into() })?;
Ok(data)
}
}

View File

@ -5,7 +5,7 @@ use std::collections::HashMap;
use crate::Username; use crate::Username;
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SSOWatConfig { pub struct SSOWatConfig {
domains: Vec<String>, domains: Vec<String>,
permissions: HashMap<PermissionName, Permission>, permissions: HashMap<PermissionName, Permission>,
@ -32,7 +32,7 @@ impl SSOWatConfig {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Permission { pub struct Permission {
auth_header: bool, auth_header: bool,
label: String, label: String,
@ -43,7 +43,7 @@ pub struct Permission {
users: Vec<Username>, users: Vec<Username>,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct PermissionName { pub struct PermissionName {
#[serde(flatten)] #[serde(flatten)]
name: String, name: String,