Can set Matrix access token

This commit is contained in:
Pierre HUBERT 2025-01-23 21:28:33 +01:00
parent 78700d0be5
commit bfbc2a690b
9 changed files with 206 additions and 10 deletions

2
Cargo.lock generated
View File

@ -1616,7 +1616,9 @@ dependencies = [
"rust-embed",
"rust-s3",
"serde",
"serde_json",
"thiserror 2.0.11",
"urlencoding",
]
[[package]]

View File

@ -10,7 +10,8 @@ clap = { version = "4.5.26", features = ["derive", "env"] }
lazy_static = "1.5.0"
anyhow = "1.0.95"
serde = { version = "1.0.217", features = ["derive"] }
rust-s3 = "0.36.0-beta.2"
serde_json = "1.0.137"
rust-s3 = { version = "0.36.0-beta.2", features = ["tokio"] }
actix-web = "4"
actix-session = { version = "0.10.1", features = ["redis-session"] }
light-openid = "1.0.2"
@ -18,4 +19,5 @@ thiserror = "2.0.11"
rand = "0.9.0-beta.3"
rust-embed = "8.5.0"
mime_guess = "2.0.5"
askama = "0.12.1"
askama = "0.12.1"
urlencoding = "2.1.3"

View File

@ -75,11 +75,11 @@ pub struct AppConfig {
s3_endpoint: String,
/// S3 access key
#[arg(long, env, default_value = "topsecret")]
#[arg(long, env, default_value = "minioadmin")]
s3_access_key: String,
/// S3 secret key
#[arg(long, env, default_value = "topsecret")]
#[arg(long, env, default_value = "minioadmin")]
s3_secret_key: String,
/// S3 skip auto create bucket if not existing

View File

@ -4,11 +4,16 @@ use actix_web::cookie::Key;
use actix_web::{web, App, HttpServer};
use matrix_gateway::app_config::AppConfig;
use matrix_gateway::server::web_ui;
use matrix_gateway::user::UserConfig;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
UserConfig::create_bucket_if_required()
.await
.expect("Failed to create bucket!");
// FIXME : not scalable
let secret_key = Key::generate();
@ -33,6 +38,7 @@ async fn main() -> std::io::Result<()> {
// Web configuration routes
.route("/assets/{tail:.*}", web::get().to(web_ui::static_file))
.route("/", web::get().to(web_ui::home))
.route("/", web::post().to(web_ui::home))
.route("/oidc_cb", web::get().to(web_ui::oidc_cb))
.route("/sign_out", web::get().to(web_ui::sign_out))

View File

@ -16,6 +16,8 @@ pub enum HttpFailure {
SessionError(#[from] actix_session::SessionGetError),
#[error("an unspecified open id error occurred: {0}")]
OpenID(Box<dyn Error>),
#[error("an error occurred while fetching user configuration: {0}")]
FetchUserConfig(anyhow::Error),
#[error("an unspecified internal error occurred: {0}")]
InternalError(#[from] anyhow::Error),
}

View File

@ -1,7 +1,7 @@
use crate::app_config::AppConfig;
use crate::constants::{STATE_KEY, USER_SESSION_KEY};
use crate::server::{HttpFailure, HttpResult};
use crate::user::{User, UserID};
use crate::user::{User, UserConfig, UserID};
use crate::utils;
use actix_session::Session;
use actix_web::{web, HttpResponse};
@ -32,10 +32,21 @@ pub async fn static_file(path: web::Path<String>) -> HttpResult {
struct HomeTemplate {
name: String,
matrix_token: String,
success_message: Option<String>,
error_message: Option<String>,
}
/// Update matrix token request
#[derive(serde::Deserialize)]
pub struct UpdateMatrixToken {
new_matrix_token: Option<String>,
}
/// Main route
pub async fn home(session: Session) -> HttpResult {
pub async fn home(
session: Session,
update_matrix_token: Option<web::Form<UpdateMatrixToken>>,
) -> HttpResult {
// Get user information, requesting authentication if information is missing
let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else {
// Generate auth state
@ -54,12 +65,37 @@ pub async fn home(session: Session) -> HttpResult {
.finish());
};
let mut success_message = None;
let mut error_message = None;
// Retrieve user configuration
let mut config = UserConfig::load(&user.id)
.await
.map_err(HttpFailure::FetchUserConfig)?;
// Update matrix token, if requested
if let Some(update_matrix_token) = update_matrix_token {
if let Some(t) = update_matrix_token.0.new_matrix_token {
if t.len() < 3 {
error_message = Some("Specified Matrix token is too short!".to_string());
} else {
// TODO : invalidate all existing connections
config.matrix_token = t;
config.save().await?;
success_message = Some("Matrix token was successfully updated!".to_string());
}
}
}
// Render page
Ok(HttpResponse::Ok()
.insert_header(("content-type", "text/html"))
.body(
HomeTemplate {
name: user.name,
matrix_token: "TODO".to_string(),
matrix_token: config.obfuscated_matrix_token(),
success_message,
error_message,
}
.render()
.unwrap(),

View File

@ -1,9 +1,135 @@
use crate::app_config::AppConfig;
use crate::utils::curr_time;
use s3::error::S3Error;
use s3::request::ResponseData;
use s3::{Bucket, BucketConfiguration};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum UserError {
#[error("failed to fetch user configuration: {0}")]
FetchUserConfig(S3Error),
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct UserID(pub String);
impl UserID {
fn conf_path_in_bucket(&self) -> String {
format!("confs/{}.json", urlencoding::encode(&self.0))
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct User {
pub id: UserID,
pub name: String,
pub email: String,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct UserConfig {
/// Target user ID
pub user_id: UserID,
/// Configuration creation time
pub created: u64,
/// Configuration last update time
pub updated: u64,
/// Current user matrix token
pub matrix_token: String,
}
impl UserConfig {
/// Create S3 bucket if required
pub async fn create_bucket_if_required() -> anyhow::Result<()> {
if AppConfig::get().s3_skip_auto_create_bucket {
log::debug!("Skipping bucket existence check");
return Ok(());
}
let bucket = AppConfig::get().s3_bucket()?;
match bucket.location().await {
Ok(_) => {
log::debug!("The bucket already exists.");
return Ok(());
}
Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchKey</Code>") => {
log::warn!("Failed to fetch bucket location, but it seems that bucket exists.");
return Ok(());
}
Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchBucket</Code>") => {
log::warn!("The bucket does not seem to exists, trying to create it!")
}
Err(e) => {
log::error!("Got unexpected error when querying bucket info: {}", e);
return Err(e.into());
}
}
Bucket::create_with_path_style(
&bucket.name,
bucket.region,
AppConfig::get().s3_credentials()?,
BucketConfiguration::private(),
)
.await?;
Ok(())
}
/// Get current user configuration
pub async fn load(user_id: &UserID) -> anyhow::Result<Self> {
let res: Result<ResponseData, S3Error> = AppConfig::get()
.s3_bucket()?
.get_object(user_id.conf_path_in_bucket())
.await;
match res {
Ok(res) => Ok(serde_json::from_slice(res.as_slice())?),
Err(S3Error::HttpFailWithBody(404, _)) => {
log::warn!("User configuration does not exists, generating a new one...");
Ok(Self {
user_id: user_id.clone(),
created: curr_time()?,
updated: curr_time()?,
matrix_token: "".to_string(),
})
}
Err(e) => Err(UserError::FetchUserConfig(e).into()),
}
}
/// Set user configuration
pub async fn save(&mut self) -> anyhow::Result<()> {
log::info!("Saving new configuration for user {:?}", self.user_id);
self.updated = curr_time()?;
// Save updated configuration
AppConfig::get()
.s3_bucket()?
.put_object(
self.user_id.conf_path_in_bucket(),
&serde_json::to_vec(self)?,
)
.await?;
Ok(())
}
/// Get current user matrix token, in an obfuscated form
pub fn obfuscated_matrix_token(&self) -> String {
self.matrix_token
.chars()
.enumerate()
.map(|(num, c)| match num {
0 | 1 => c,
_ => 'X',
})
.collect()
}
}

View File

@ -1,6 +1,14 @@
use rand::distr::{Alphanumeric, SampleString};
use std::time::{SystemTime, UNIX_EPOCH};
// Generate a random string of a given size
/// Generate a random string of a given size
pub fn rand_str(len: usize) -> String {
Alphanumeric.sample_string(&mut rand::rng(), len)
}
/// Get current time
pub fn curr_time() -> anyhow::Result<u64> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|t| t.as_secs())?)
}

View File

@ -32,6 +32,20 @@
<div class="body-content">
<!-- Success message -->
{% if let Some(msg) = success_message %}
<div class="alert alert-success">
{{ msg }}
</div>
{% endif %}
<!-- Error message -->
{% if let Some(msg) = error_message %}
<div class="alert alert-danger">
{{ msg }}
</div>
{% endif %}
<!-- Matrix authentication token -->
<div class="card border-light mb-3">
<div class="card-header">Matrix authentication token</div>
@ -50,11 +64,11 @@
<p>Tip: you can rename the session to easily identify it among all your other sessions!</p>
<form action="/set_matrix_access_token">
<form action="/" method="post">
<div>
<label for="accessTokenInput" class="form-label mt-4">New Matrix access token</label>
<input type="text" class="form-control" id="accessTokenInput" aria-describedby="tokenHelp"
placeholder="{{ matrix_token }}" required minlength="2"/>
placeholder="{{ matrix_token }}" required minlength="2" name="new_matrix_token"/>
<small id="tokenHelp" class="form-text text-muted">Changing this value will reset all active
connections
to Matrix GW.</small>