Can register new clients

This commit is contained in:
Pierre HUBERT 2025-01-27 21:31:33 +01:00
parent bfbc2a690b
commit 28b64b4475
6 changed files with 114 additions and 18 deletions

15
Cargo.lock generated
View File

@ -1483,6 +1483,9 @@ name = "ipnet"
version = "2.11.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
@ -1608,6 +1611,7 @@ dependencies = [
"askama", "askama",
"clap", "clap",
"env_logger", "env_logger",
"ipnet",
"lazy_static", "lazy_static",
"light-openid", "light-openid",
"log", "log",
@ -1619,6 +1623,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror 2.0.11", "thiserror 2.0.11",
"urlencoding", "urlencoding",
"uuid",
] ]
[[package]] [[package]]
@ -2887,6 +2892,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom 0.2.15",
"serde",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View File

@ -21,3 +21,5 @@ rust-embed = "8.5.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
askama = "0.12.1" askama = "0.12.1"
urlencoding = "2.1.3" urlencoding = "2.1.3"
uuid = { version = "1.12.1", features = ["v4", "serde"] }
ipnet = { version = "2.11.0", features = ["serde"] }

View File

@ -1,8 +1,3 @@
# This compose file is compatible with Compose itself, it might need some
# adjustments to run properly with stack.
version: "3"
services: services:
synapse: synapse:
image: docker.io/matrixdotorg/synapse:latest image: docker.io/matrixdotorg/synapse:latest

View File

@ -1,12 +1,14 @@
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::constants::{STATE_KEY, USER_SESSION_KEY}; use crate::constants::{STATE_KEY, USER_SESSION_KEY};
use crate::server::{HttpFailure, HttpResult}; use crate::server::{HttpFailure, HttpResult};
use crate::user::{User, UserConfig, UserID}; use crate::user::{APIClient, User, UserConfig, UserID};
use crate::utils; use crate::utils;
use actix_session::Session; use actix_session::Session;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use askama::Template; use askama::Template;
use ipnet::IpNet;
use light_openid::primitives::OpenIDConfig; use light_openid::primitives::OpenIDConfig;
use std::str::FromStr;
/// Static assets /// Static assets
#[derive(rust_embed::Embed)] #[derive(rust_embed::Embed)]
@ -36,17 +38,21 @@ struct HomeTemplate {
error_message: Option<String>, error_message: Option<String>,
} }
/// Update matrix token request /// HTTP form request
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UpdateMatrixToken { pub struct FormRequest {
/// Update matrix token
new_matrix_token: Option<String>, new_matrix_token: Option<String>,
/// Create a new client
new_client_desc: Option<String>,
/// Restrict new client to a given network
ip_network: Option<String>,
} }
/// Main route /// Main route
pub async fn home( pub async fn home(session: Session, form_req: Option<web::Form<FormRequest>>) -> HttpResult {
session: Session,
update_matrix_token: Option<web::Form<UpdateMatrixToken>>,
) -> HttpResult {
// Get user information, requesting authentication if information is missing // Get user information, requesting authentication if information is missing
let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else { let Some(user): Option<User> = session.get(USER_SESSION_KEY)? else {
// Generate auth state // Generate auth state
@ -73,9 +79,9 @@ pub async fn home(
.await .await
.map_err(HttpFailure::FetchUserConfig)?; .map_err(HttpFailure::FetchUserConfig)?;
if let Some(form_req) = form_req {
// Update matrix token, if requested // Update matrix token, if requested
if let Some(update_matrix_token) = update_matrix_token { if let Some(t) = form_req.0.new_matrix_token {
if let Some(t) = update_matrix_token.0.new_matrix_token {
if t.len() < 3 { if t.len() < 3 {
error_message = Some("Specified Matrix token is too short!".to_string()); error_message = Some("Specified Matrix token is too short!".to_string());
} else { } else {
@ -85,6 +91,28 @@ pub async fn home(
success_message = Some("Matrix token was successfully updated!".to_string()); success_message = Some("Matrix token was successfully updated!".to_string());
} }
} }
// Create a new client, if requested
if let Some(new_token_desc) = form_req.0.new_client_desc {
let ip_net = match form_req.0.ip_network.as_deref() {
None | Some("") => None,
Some(e) => match IpNet::from_str(e) {
Ok(n) => Some(n),
Err(e) => {
log::error!("Failed to parse IP network provided by user: {e}");
error_message = Some(format!("Failed to parse restricted IP network: {e}"));
None
}
},
};
if error_message.is_none() {
let token = APIClient::generate(new_token_desc, ip_net);
success_message = Some(format!("The secret of your new token is '{}'. Be sure to write it somewhere as you will not be able to recover it later!", token.secret));
config.clients.push(token);
config.save().await?;
}
}
} }
// Render page // Render page

View File

@ -1,10 +1,11 @@
use crate::app_config::AppConfig;
use crate::utils::curr_time;
use s3::error::S3Error; use s3::error::S3Error;
use s3::request::ResponseData; use s3::request::ResponseData;
use s3::{Bucket, BucketConfiguration}; use s3::{Bucket, BucketConfiguration};
use thiserror::Error; use thiserror::Error;
use crate::app_config::AppConfig;
use crate::utils::{curr_time, rand_str};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum UserError { pub enum UserError {
#[error("failed to fetch user configuration: {0}")] #[error("failed to fetch user configuration: {0}")]
@ -27,6 +28,34 @@ pub struct User {
pub email: String, pub email: String,
} }
/// Single API client information
#[derive(serde::Serialize, serde::Deserialize)]
pub struct APIClient {
/// Client unique ID
pub id: uuid::Uuid,
/// Client description
pub description: String,
/// Restricted API network for token
pub network: Option<ipnet::IpNet>,
/// Client secret
pub secret: String,
}
impl APIClient {
/// Generate a new API client
pub fn generate(description: String, network: Option<ipnet::IpNet>) -> Self {
Self {
id: Default::default(),
description,
network,
secret: rand_str(20),
}
}
}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct UserConfig { pub struct UserConfig {
/// Target user ID /// Target user ID
@ -40,6 +69,9 @@ pub struct UserConfig {
/// Current user matrix token /// Current user matrix token
pub matrix_token: String, pub matrix_token: String,
/// API clients
pub clients: Vec<APIClient>,
} }
impl UserConfig { impl UserConfig {
@ -97,6 +129,7 @@ impl UserConfig {
created: curr_time()?, created: curr_time()?,
updated: curr_time()?, updated: curr_time()?,
matrix_token: "".to_string(), matrix_token: "".to_string(),
clients: vec![],
}) })
} }
Err(e) => Err(UserError::FetchUserConfig(e).into()), Err(e) => Err(UserError::FetchUserConfig(e).into()),

View File

@ -46,6 +46,29 @@
</div> </div>
{% endif %} {% endif %}
<!-- New client -->
<div class="card border-light mb-3">
<div class="card-header">New client</div>
<div class="card-body">
<form action="/" method="post">
<div>
<label for="new_client_desc" class="form-label">Description</label>
<input type="text" class="form-control" id="new_client_desc" required minlength="3"
aria-describedby="new_client_desc" placeholder="New client description..." name="new_client_desc" />
<small class="form-text text-muted">Client description helps with identification.</small>
</div>
<div>
<label for="ip_network" class="form-label">Allowed IP network</label>
<input type="text" class="form-control" id="ip_network" aria-describedby="ip_network"
placeholder="Client network (x.x.x.x/x or x:x:x:x:x:x/x" name="ip_network" />
<small class="form-text text-muted">Restrict the networks this IP address can be used from.</small>
</div>
<input type="submit" class="btn btn-primary" value="Create client"/>
</form>
</div>
</div>
<!-- Matrix authentication token --> <!-- Matrix authentication token -->
<div class="card border-light mb-3"> <div class="card border-light mb-3">
<div class="card-header">Matrix authentication token</div> <div class="card-header">Matrix authentication token</div>