From 54867e301b462732d2283a176c09fb71f1379fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Bardon?= Date: Thu, 30 May 2024 00:27:01 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Introduce=20ephemeral=20access=20lo?= =?UTF-8?q?gs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Cargo.lock | 2 +- src/orangutan-server/Cargo.toml | 2 +- .../src/routes/debug_routes.rs | 81 ++++++++++++++----- src/orangutan-server/src/routes/main_route.rs | 16 +++- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index a9415e8..3ec559b 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1021,7 +1021,7 @@ dependencies = [ [[package]] name = "orangutan-server" -version = "0.4.3" +version = "0.4.4" dependencies = [ "base64 0.22.1", "biscuit-auth", diff --git a/src/orangutan-server/Cargo.toml b/src/orangutan-server/Cargo.toml index 0098640..5282066 100644 --- a/src/orangutan-server/Cargo.toml +++ b/src/orangutan-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orangutan-server" -version = "0.4.3" +version = "0.4.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/orangutan-server/src/routes/debug_routes.rs b/src/orangutan-server/src/routes/debug_routes.rs index 2d4f7c4..e1d5030 100644 --- a/src/orangutan-server/src/routes/debug_routes.rs +++ b/src/orangutan-server/src/routes/debug_routes.rs @@ -1,7 +1,4 @@ -use std::{ - fmt::Display, - sync::{Arc, RwLock}, -}; +use std::sync::{Arc, RwLock}; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; @@ -19,10 +16,19 @@ lazy_static! { /// /// // NOTE: `Arc` prevents race conditions pub(crate) static ref ERRORS: Arc>> = Arc::default(); + /// Access logs, per "user". + /// + /// // NOTE: `Arc` prevents race conditions + pub(crate) static ref ACCESS_LOGS: Arc>> = Arc::default(); } pub(super) fn routes() -> Vec { - routes![clear_cookies, get_user_info, errors] + routes![ + clear_cookies, + get_user_info, + errors, + access_logs + ] } #[get("/clear-cookies")] @@ -54,26 +60,61 @@ pub struct ErrorLog { pub line: String, } -impl Display for ErrorLog { - fn fmt( - &self, - f: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { - write!(f, "{} | {}", self.timestamp, self.line) +#[get("/_errors")] +fn errors(token: Token) -> Result { + if !token.profiles().contains(&"*".to_owned()) { + Err(Status::Unauthorized)? + } + + let mut res = String::new(); + for log in ERRORS.read().unwrap().iter() { + res.push_str(&format!("{} | {}", log.timestamp, log.line)); } + + Ok(res) } -#[get("/_errors")] -fn errors(token: Token) -> Result { +/// In Orangutan, users are a list of profiles. +/// +/// NOTE: One day we will introduce `user` facts in Biscuit tokens +/// to differenciate the unique name from profiles. +/// That day we will change this type to just `String`. +type User = Vec; + +pub struct AccessLog { + pub timestamp: DateTime, + pub user: User, + pub path: String, +} + +#[get("/_access-logs")] +fn access_logs(token: Token) -> Result { if !token.profiles().contains(&"*".to_owned()) { Err(Status::Unauthorized)? } - Ok(ERRORS - .read() - .unwrap() - .iter() - .map(ToString::to_string) - .collect::>() - .join("\n")) + let mut res = String::new(); + for log in ACCESS_LOGS.read().unwrap().iter() { + let mut user = log.user.clone(); + user.sort(); + res.push_str(&format!( + "{} | {}: {}\n", + log.timestamp, + user.join(","), + log.path + )); + } + + Ok(res) +} + +pub fn log_access( + user: User, + path: String, +) { + ACCESS_LOGS.write().unwrap().push(AccessLog { + timestamp: Utc::now(), + user, + path, + }) } diff --git a/src/orangutan-server/src/routes/main_route.rs b/src/orangutan-server/src/routes/main_route.rs index 3651f86..dca923d 100644 --- a/src/orangutan-server/src/routes/main_route.rs +++ b/src/orangutan-server/src/routes/main_route.rs @@ -3,10 +3,14 @@ use std::{path::Path, time::SystemTime}; use biscuit_auth::macros::authorizer; use object_reader::{ObjectReader, ReadObjectResponse}; use orangutan_helpers::{data_file, read_allowed, readers::object_reader, website_id::WebsiteId}; -use rocket::{get, http::uri::Origin, routes, Route, State}; +use rocket::{ + get, + http::{uri::Origin, Accept}, + routes, Route, State, +}; use tracing::{debug, trace}; -use crate::{config::*, request_guards::Token, util::error}; +use crate::{config::*, request_guards::Token, routes::debug_routes::log_access, util::error}; pub(super) fn routes() -> Vec { routes![handle_request] @@ -17,6 +21,7 @@ async fn handle_request( origin: &Origin<'_>, token: Option, object_reader: &State, + accept: Option<&Accept>, ) -> Result, crate::Error> { // FIXME: Handle error let path = urlencoding::decode(origin.path().as_str()) @@ -28,6 +33,13 @@ async fn handle_request( debug!("User has profiles {user_profiles:?}"); let website_id = WebsiteId::from(&user_profiles); + // Log access only if the page is HTML. + // WARN: This solution is far from perfect as someone requesting a page without setting the `Accept` header + // would not be logged even though they'd get the file back. + if accept.is_some_and(|a| a.media_types().find(|t| t.is_html()).is_some()) { + log_access(user_profiles.to_owned(), path.to_owned()); + } + let stored_objects: Vec = object_reader .list_objects(&path, &website_id)