Skip to content

Commit

Permalink
✨ Add admin route: Access token generator
Browse files Browse the repository at this point in the history
  • Loading branch information
RemiBardon committed Jun 8, 2024
1 parent 103eb4a commit 2916f9d
Show file tree
Hide file tree
Showing 12 changed files with 817 additions and 25 deletions.
348 changes: 344 additions & 4 deletions src/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ iso8601-duration = "0.2"
lazy_static = "1"
rocket = "0.5"
serde_json = "1"
tera = "1"
thiserror = "1"
time = "0.3"
tracing = "0.1"
Expand Down
9 changes: 8 additions & 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.5"
version = "0.4.6"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -11,10 +11,17 @@ biscuit-auth = { workspace = true }
chrono = { workspace = true }
lazy_static = { workspace = true }
orangutan-helpers = { path = "../helpers" }
orangutan-refresh-token = { path = "../orangutan-refresh-token" }
rocket = { workspace = true }
serde_json = { workspace = true }
tera = { workspace = true, optional = true }
thiserror = { workspace = true }
time = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
urlencoding = { workspace = true }

[features]
default = ["token-generator"]
templating = ["tera"]
token-generator = ["templating"]
79 changes: 72 additions & 7 deletions src/orangutan-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ use rocket::{
response::{self, Responder},
Request,
};
use routes::{main_route, update_content_routes};
#[cfg(feature = "templating")]
use tracing::debug;
use tracing::warn;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use util::error;

use crate::config::NOT_FOUND_FILE;
#[cfg(feature = "templating")]
use crate::util::templating;
use crate::{
config::NOT_FOUND_FILE,
routes::{main_route, update_content_routes},
util::error,
};

#[rocket::launch]
fn rocket() -> _ {
rocket::build()
let rocket = rocket::build()
.mount("/", routes::routes())
.register("/", catchers![unauthorized, not_found])
.manage(ObjectReader::new())
Expand All @@ -45,7 +52,23 @@ fn rocket() -> _ {
rocket.shutdown().notify();
}
})
}))
}));

// Add support for templating if needed
#[cfg(feature = "templating")]
let rocket = rocket.attach(AdHoc::on_ignite(
"Initialize templating engine",
|rocket| async move {
let mut tera = tera::Tera::default();
if let Err(err) = tera.add_raw_templates(routes::templates()) {
tracing::error!("{err}");
std::process::exit(1)
}
rocket.manage(tera)
},
));

rocket
}

fn liftoff() -> Result<(), Error> {
Expand Down Expand Up @@ -91,6 +114,17 @@ enum Error {
MainRouteError(#[from] main_route::Error),
#[error("Could not update content: {0}")]
UpdateContentError(#[from] update_content_routes::Error),
#[error("Unauthorized")]
Unauthorized,
#[cfg(feature = "templating")]
#[error("Templating error: {0}")]
TemplatingError(#[from] templating::Error),
#[cfg(feature = "templating")]
#[error("Internal server error: {0}")]
InternalServerError(String),
#[cfg(feature = "templating")]
#[error("Client error: {0}")]
ClientError(String),
}

#[rocket::async_trait]
Expand All @@ -99,7 +133,38 @@ impl<'r> Responder<'r, 'static> for Error {
self,
_: &'r Request<'_>,
) -> response::Result<'static> {
error(format!("{self}"));
Err(Status::InternalServerError)
match self {
Self::Unauthorized => {
warn!("{self}");
Err(Status::Unauthorized)
},
#[cfg(feature = "templating")]
Self::ClientError(_) => {
debug!("{self}");
Err(Status::BadRequest)
},
_ => {
error(format!("{self}"));
Err(Status::InternalServerError)
},
}
}
}

#[cfg(feature = "templating")]
impl From<orangutan_refresh_token::Error> for Error {
fn from(err: orangutan_refresh_token::Error) -> Self {
match err {
orangutan_refresh_token::Error::CannotAddFact(_, _)
| orangutan_refresh_token::Error::CannotBuildBiscuit(_)
| orangutan_refresh_token::Error::CannotAddBlock(_, _)
| orangutan_refresh_token::Error::CannotConvertToBase64(_) => {
Self::InternalServerError(format!("Token generation error: {err}"))
},
orangutan_refresh_token::Error::MalformattedDuration(_, _)
| orangutan_refresh_token::Error::UnsupportedDuration(_) => {
Self::ClientError(format!("Invalid token data: {err}"))
},
}
}
}
116 changes: 103 additions & 13 deletions src/orangutan-server/src/routes/debug_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ use std::sync::{Arc, RwLock};

use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use rocket::{
get,
http::{CookieJar, Status},
routes, Route,
};
use rocket::{get, http::CookieJar, routes, Route};

use crate::request_guards::Token;
use crate::{request_guards::Token, Error};

lazy_static! {
/// A list of runtime errors, used to show error logs in an admin page
Expand All @@ -23,12 +19,27 @@ lazy_static! {
}

pub(super) fn routes() -> Vec<Route> {
routes![
let routes = routes![
clear_cookies,
get_user_info,
errors,
access_logs
]
access_logs,
];
#[cfg(feature = "token-generator")]
let routes = vec![routes, routes![
token_generator::token_generation_form,
token_generator::generate_token,
]]
.concat();
routes
}

#[cfg(feature = "templating")]
pub(super) fn templates() -> Vec<(&'static str, &'static str)> {
vec![(
"generate-token.html",
include_str!("templates/generate-token.html.tera"),
)]
}

#[get("/clear-cookies")]
Expand Down Expand Up @@ -61,9 +72,9 @@ pub struct ErrorLog {
}

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

let mut res = String::new();
Expand All @@ -88,9 +99,9 @@ pub struct AccessLog {
}

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

let mut res = String::new();
Expand Down Expand Up @@ -125,3 +136,82 @@ pub fn log_access(
path,
})
}

#[cfg(feature = "token-generator")]
pub mod token_generator {
use orangutan_refresh_token::RefreshToken;
use rocket::{
form::{Form, Strict},
get, post,
response::content::RawHtml,
FromForm, State,
};

use crate::{
context,
request_guards::Token,
util::{templating::render, WebsiteRoot},
Error,
};

fn token_generation_form_(
tera: &State<tera::Tera>,
link: Option<String>,
base_url: &str,
) -> Result<RawHtml<String>, Error> {
let html = render(
tera,
"generate-token.html",
context! { page_title: "Access token generator", link, base_url },
)?;

Ok(RawHtml(html))
}

#[get("/_generate-token")]
pub fn token_generation_form(
token: Token,
tera: &State<tera::Tera>,
website_root: WebsiteRoot,
) -> Result<RawHtml<String>, Error> {
if !token.profiles().contains(&"*".to_owned()) {
Err(Error::Unauthorized)?
}

token_generation_form_(tera, None, &website_root)
}

#[derive(FromForm)]
pub struct GenerateTokenForm {
ttl: String,
name: String,
profiles: String,
url: String,
}

#[post("/_generate-token", data = "<form>")]
pub fn generate_token(
token: Token,
tera: &State<tera::Tera>,
form: Form<Strict<GenerateTokenForm>>,
website_root: WebsiteRoot,
) -> Result<RawHtml<String>, Error> {
if !token.profiles().contains(&"*".to_owned()) {
Err(Error::Unauthorized)?
}

let mut profiles = vec![form.name.to_owned()];
profiles.append(&mut form.profiles.split(",").map(ToOwned::to_owned).collect());
if profiles.contains(&"*".to_string()) {
Err(Error::ClientError(format!(
"Profiles cannot contain '*' (got {profiles:?})."
)))?
}

let token = RefreshToken::try_from(form.ttl.to_owned(), profiles.into_iter())?;
let token_base64 = token.as_base64()?;
let link = format!("{}?refresh_token={token_base64}", form.url);

token_generation_form_(tera, Some(link), &website_root)
}
}
9 changes: 9 additions & 0 deletions src/orangutan-server/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ pub(super) fn routes() -> Vec<Route> {
]
.concat()
}

#[cfg(feature = "templating")]
pub(super) fn templates() -> Vec<(&'static str, &'static str)> {
vec![
vec![("base.html", include_str!("templates/base.html.tera"))],
debug_routes::templates(),
]
.concat()
}
7 changes: 7 additions & 0 deletions src/orangutan-server/src/routes/templates/.zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
{
"tab_size": 2
}
39 changes: 39 additions & 0 deletions src/orangutan-server/src/routes/templates/base.html.tera
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ page_title }}</title>
<style>
{% block style %}
body {
--text-color: #111;
--bg-color: #EEE;
}

@media (prefers-color-scheme: dark) {
body {
--text-color: #EEE;
--bg-color: #111;
}
}

body {
color: var(--text-color);
background: var(--bg-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}

a {
color: var(--text-color);
}
{% endblock style %}
</style>
</head>
<body>
<h1>{{ page_title }}</h1>
<main>
{% block main %}{% endblock main %}
</main>
</body>
</html>
Loading

0 comments on commit 2916f9d

Please sign in to comment.