Skip to content

Commit

Permalink
✨ Introduce ephemeral access logs
Browse files Browse the repository at this point in the history
  • Loading branch information
RemiBardon committed May 29, 2024
1 parent e4cad5d commit 54867e3
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/orangutan-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
81 changes: 61 additions & 20 deletions src/orangutan-server/src/routes/debug_routes.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,10 +16,19 @@ lazy_static! {
///
/// // NOTE: `Arc` prevents race conditions
pub(crate) static ref ERRORS: Arc<RwLock<Vec<ErrorLog>>> = Arc::default();
/// Access logs, per "user".
///
/// // NOTE: `Arc` prevents race conditions
pub(crate) static ref ACCESS_LOGS: Arc<RwLock<Vec<AccessLog>>> = Arc::default();
}

pub(super) fn routes() -> Vec<Route> {
routes![clear_cookies, get_user_info, errors]
routes![
clear_cookies,
get_user_info,
errors,
access_logs
]
}

#[get("/clear-cookies")]
Expand Down Expand Up @@ -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<String, Status> {
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<String, Status> {
/// 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<String>;

pub struct AccessLog {
pub timestamp: DateTime<Utc>,
pub user: User,
pub path: String,
}

#[get("/_access-logs")]
fn access_logs(token: Token) -> Result<String, Status> {
if !token.profiles().contains(&"*".to_owned()) {
Err(Status::Unauthorized)?
}

Ok(ERRORS
.read()
.unwrap()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.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,
})
}
16 changes: 14 additions & 2 deletions src/orangutan-server/src/routes/main_route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Route> {
routes![handle_request]
Expand All @@ -17,6 +21,7 @@ async fn handle_request(
origin: &Origin<'_>,
token: Option<Token>,
object_reader: &State<ObjectReader>,
accept: Option<&Accept>,
) -> Result<Option<ReadObjectResponse>, crate::Error> {
// FIXME: Handle error
let path = urlencoding::decode(origin.path().as_str())
Expand All @@ -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<String> =
object_reader
.list_objects(&path, &website_id)
Expand Down

0 comments on commit 54867e3

Please sign in to comment.