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"
tokio = { version = "1", 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" ] }
snafu = "0.7"
log = "0.4"
@ -20,7 +20,8 @@ env_logger = "0.10"
ring = "0.16"
hex = "0.4"
chrono = { version = "0.4", features = [ "serde" ] }
yunohost-api = { path = "yunohost-api" }
yunohost-api = { path = "yunohost-api", features = [ "axum" ] }
axum_typed_multipart = "0.8"
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))]
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 axum;
#[macro_use] extern crate log;
#[macro_use] extern crate serde;
use clap::Parser;

View File

@ -1,16 +1,19 @@
use axum::{
extract::{FromRequest, Form, Json, Query},
extract::{FromRequest, Form, Json, Query, State},
http::{self, Request, StatusCode},
response::{IntoResponse, Response},
RequestExt,
};
use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
use yunohost_api::{Username, Password};
use crate::state::RoutableAppState;
#[derive(Debug, TryFromMultipart, Deserialize)]
pub struct LoginForm {
username: String,
username: Username,
#[allow(dead_code)]
password: String,
password: Password,
}
#[async_trait]
@ -52,6 +55,10 @@ where
}
#[debug_handler]
pub async fn route(form: LoginForm) -> String {
format!("Welcome {}", form.username)
pub async fn route(state: State<RoutableAppState>, form: LoginForm) -> String {
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 {
let app = Router::new()
.route("/", get(index::route))
.route("/login/", get(login::route))
.route("/login/", post(login::route))
.with_state(state);
if let Some(p) = subpath {
Router::new()

View File

@ -1,5 +1,5 @@
use snafu::prelude::*;
use yunohost_api::YunohostUsers;
use yunohost_api::{YunohostUsers, Username, Password};
use std::sync::Arc;
@ -23,4 +23,8 @@ impl AppState {
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 socket;

View File

@ -21,7 +21,10 @@ use tokio::{
};
use tower::BoxError;
use crate::error::*;
use crate::{
error::*,
utils::fs::FSPermissions,
};
pub struct ServerAccept {
uds: UnixListener,
@ -118,7 +121,9 @@ pub async fn serve(path: &Path, app: Router) -> Result<(), Error> {
.await
.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())
.context(SocketCreateSnafu { path: path.clone() })?;

View File

@ -15,7 +15,13 @@ serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
regex = "1.9"
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]
scan-rules = "0.2"
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 serde::{Serialize, Deserialize};
use snafu::OptionExt;
use snafu::prelude::*;
#[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;
@ -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);
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);
impl Password {
@ -81,3 +114,25 @@ impl FromStr for Password {
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;
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SSOWatConfig {
domains: Vec<String>,
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 {
auth_header: bool,
label: String,
@ -43,7 +43,7 @@ pub struct Permission {
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 {
#[serde(flatten)]
name: String,