Add users authentication routes
This commit is contained in:
@@ -20,7 +20,7 @@ docker run --rm -it docker.io/pierre42100/matrix_gateway --help
|
|||||||
### Dependencies
|
### Dependencies
|
||||||
```
|
```
|
||||||
cd matrixgw_backend
|
cd matrixgw_backend
|
||||||
mkdir -p storage/maspostgres storage/synapse storage/minio
|
mkdir -p storage/maspostgres storage/synapse
|
||||||
docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -35,14 +35,12 @@ URLs:
|
|||||||
* Synapse: http://localhost:8448/
|
* Synapse: http://localhost:8448/
|
||||||
* Matrix Authentication Service: http://localhost:8778/
|
* Matrix Authentication Service: http://localhost:8778/
|
||||||
* OpenID configuration: http://127.0.0.1:9001/dex/.well-known/openid-configuration
|
* OpenID configuration: http://127.0.0.1:9001/dex/.well-known/openid-configuration
|
||||||
* Minio console: http://localhost:9002/
|
|
||||||
|
|
||||||
Auto-created Matrix accounts:
|
Auto-created Matrix accounts:
|
||||||
|
|
||||||
* `admin1` : `admin1`
|
* `admin1` : `admin1`
|
||||||
* `user1` : `user1`
|
* `user1` : `user1`
|
||||||
|
|
||||||
Minio administration credentials: `minioadmin` : `minioadmin`
|
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
1
matrixgw_backend/.gitignore
vendored
1
matrixgw_backend/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
storage
|
storage
|
||||||
|
app_storage
|
||||||
.idea
|
.idea
|
||||||
target
|
target
|
||||||
|
|||||||
948
matrixgw_backend/Cargo.lock
generated
948
matrixgw_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,20 @@ clap = { version = "4.5.51", features = ["derive", "env"] }
|
|||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
rust-s3 = { version = "0.37.0", features = ["tokio"] }
|
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
actix-web = "4.11.0"
|
actix-web = "4.11.0"
|
||||||
actix-session = { version = "0.11.0", features = ["redis-session"] }
|
actix-session = { version = "0.11.0", features = ["redis-session"] }
|
||||||
actix-remote-ip = "0.1.0"
|
actix-remote-ip = "0.1.0"
|
||||||
|
light-openid = "1.0.4"
|
||||||
|
bytes = "1.10.1"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
base16ct = { version = "0.3.0", features = ["alloc"] }
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
||||||
|
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||||
|
rand = "0.9.2"
|
||||||
|
hex = "0.4.3"
|
||||||
|
mailchecker = "6.0.19"
|
||||||
@@ -92,19 +92,6 @@ services:
|
|||||||
- ./docker/dex:/conf:ro
|
- ./docker/dex:/conf:ro
|
||||||
command: [ "dex", "serve", "/conf/dex.config.yaml" ]
|
command: [ "dex", "serve", "/conf/dex.config.yaml" ]
|
||||||
|
|
||||||
minio:
|
|
||||||
image: quay.io/minio/minio
|
|
||||||
command: minio server --console-address ":9002" /data
|
|
||||||
ports:
|
|
||||||
- 9000:9000/tcp
|
|
||||||
- 9002:9002/tcp
|
|
||||||
environment:
|
|
||||||
MINIO_ROOT_USER: minioadmin
|
|
||||||
MINIO_ROOT_PASSWORD: minioadmin
|
|
||||||
volumes:
|
|
||||||
# You may store the database tables in a local folder..
|
|
||||||
- ./storage/minio:/data
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
|
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ staticClients:
|
|||||||
- id: foo
|
- id: foo
|
||||||
secret: bar
|
secret: bar
|
||||||
redirectURIs:
|
redirectURIs:
|
||||||
- http://localhost:8000/oidc_cb
|
- http://localhost:5173/oidc_cb
|
||||||
name: Project
|
name: Project
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use jwt_simple::algorithms::HS256Key;
|
use jwt_simple::algorithms::HS256Key;
|
||||||
use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike};
|
use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike};
|
||||||
use matrix_gateway::extractors::client_auth::TokenClaims;
|
use matrixgw_backend::constants;
|
||||||
use matrix_gateway::utils::base_utils::rand_str;
|
use matrixgw_backend::extractors::auth_extractor::TokenClaims;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::os::unix::prelude::CommandExt;
|
use std::os::unix::prelude::CommandExt;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use matrixgw_backend::utils::rand_utils::rand_string;
|
||||||
|
|
||||||
/// cURL wrapper to query MatrixGW
|
/// cURL wrapper to query MatrixGW
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -59,7 +60,7 @@ fn main() {
|
|||||||
subject: None,
|
subject: None,
|
||||||
audiences: None,
|
audiences: None,
|
||||||
jwt_id: None,
|
jwt_id: None,
|
||||||
nonce: Some(rand_str(10)),
|
nonce: Some(rand_string(10)),
|
||||||
custom: TokenClaims {
|
custom: TokenClaims {
|
||||||
method: args.method.to_string(),
|
method: args.method.to_string(),
|
||||||
uri: args.uri,
|
uri: args.uri,
|
||||||
@@ -78,7 +79,7 @@ fn main() {
|
|||||||
|
|
||||||
let _ = Command::new("curl")
|
let _ = Command::new("curl")
|
||||||
.args(["-X", &args.method])
|
.args(["-X", &args.method])
|
||||||
.args(["-H", &format!("x-client-auth: {jwt}")])
|
.args(["-H", &format!("{}: {jwt}", constants::API_AUTH_HEADER)])
|
||||||
.args(args.run)
|
.args(args.run)
|
||||||
.arg(full_url)
|
.arg(full_url)
|
||||||
.exec();
|
.exec();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
use crate::users::{APITokenID, UserEmail};
|
||||||
|
use crate::utils::crypt_utils::sha256str;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use s3::creds::Credentials;
|
use std::path::{Path, PathBuf};
|
||||||
use s3::{Bucket, Region};
|
|
||||||
|
|
||||||
/// Matrix gateway backend API
|
/// Matrix gateway backend API
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@@ -11,7 +12,7 @@ pub struct AppConfig {
|
|||||||
pub listen_address: String,
|
pub listen_address: String,
|
||||||
|
|
||||||
/// Website origin
|
/// Website origin
|
||||||
#[clap(short, long, env, default_value = "http://localhost:8000")]
|
#[clap(short, long, env, default_value = "http://localhost:5173")]
|
||||||
pub website_origin: String,
|
pub website_origin: String,
|
||||||
|
|
||||||
/// Proxy IP, might end with a star "*"
|
/// Proxy IP, might end with a star "*"
|
||||||
@@ -75,29 +76,9 @@ pub struct AppConfig {
|
|||||||
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
||||||
oidc_redirect_url: String,
|
oidc_redirect_url: String,
|
||||||
|
|
||||||
/// S3 Bucket name
|
/// Application storage path
|
||||||
#[arg(long, env, default_value = "matrix-gw")]
|
#[arg(long, env, default_value = "app_storage")]
|
||||||
s3_bucket_name: String,
|
storage_path: String,
|
||||||
|
|
||||||
/// S3 region (if not using Minio)
|
|
||||||
#[arg(long, env, default_value = "eu-central-1")]
|
|
||||||
s3_region: String,
|
|
||||||
|
|
||||||
/// S3 API endpoint
|
|
||||||
#[arg(long, env, default_value = "http://localhost:9000")]
|
|
||||||
s3_endpoint: String,
|
|
||||||
|
|
||||||
/// S3 access key
|
|
||||||
#[arg(long, env, default_value = "minioadmin")]
|
|
||||||
s3_access_key: String,
|
|
||||||
|
|
||||||
/// S3 secret key
|
|
||||||
#[arg(long, env, default_value = "minioadmin")]
|
|
||||||
s3_secret_key: String,
|
|
||||||
|
|
||||||
/// S3 skip auto create bucket if not existing
|
|
||||||
#[arg(long, env)]
|
|
||||||
pub s3_skip_auto_create_bucket: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -113,10 +94,10 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get auto login email (if not empty)
|
/// Get auto login email (if not empty)
|
||||||
pub fn unsecure_auto_login_email(&self) -> Option<&str> {
|
pub fn unsecure_auto_login_email(&self) -> Option<UserEmail> {
|
||||||
match self.unsecure_auto_login_email.as_deref() {
|
match self.unsecure_auto_login_email.as_deref() {
|
||||||
None | Some("") => None,
|
None | Some("") => None,
|
||||||
s => s,
|
Some(s) => Some(UserEmail(s.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,28 +146,29 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get s3 bucket credentials
|
/// Get storage path
|
||||||
pub fn s3_credentials(&self) -> anyhow::Result<Credentials> {
|
pub fn storage_path(&self) -> &Path {
|
||||||
Ok(Credentials::new(
|
Path::new(self.storage_path.as_str())
|
||||||
Some(&self.s3_access_key),
|
|
||||||
Some(&self.s3_secret_key),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get S3 bucket
|
/// User storage directory
|
||||||
pub fn s3_bucket(&self) -> anyhow::Result<Box<Bucket>> {
|
pub fn user_directory(&self, mail: &UserEmail) -> PathBuf {
|
||||||
Ok(Bucket::new(
|
self.storage_path().join("users").join(sha256str(&mail.0))
|
||||||
&self.s3_bucket_name,
|
}
|
||||||
Region::Custom {
|
|
||||||
region: self.s3_region.to_string(),
|
/// User metadata file
|
||||||
endpoint: self.s3_endpoint.to_string(),
|
pub fn user_metadata_file_path(&self, mail: &UserEmail) -> PathBuf {
|
||||||
},
|
self.user_directory(mail).join("metadata.json")
|
||||||
self.s3_credentials()?,
|
}
|
||||||
)?
|
|
||||||
.with_path_style())
|
/// User API tokens directory
|
||||||
|
pub fn user_api_token_directory(&self, mail: &UserEmail) -> PathBuf {
|
||||||
|
self.user_directory(mail).join("api-tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User API token metadata file
|
||||||
|
pub fn user_api_token_metadata_file(&self, mail: &UserEmail, id: &APITokenID) -> PathBuf {
|
||||||
|
self.user_api_token_directory(mail).join(id.0.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
matrixgw_backend/src/constants.rs
Normal file
15
matrixgw_backend/src/constants.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/// Auth header
|
||||||
|
pub const API_AUTH_HEADER: &str = "x-client-auth";
|
||||||
|
|
||||||
|
/// Max token validity, in seconds
|
||||||
|
pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60;
|
||||||
|
|
||||||
|
/// Session-specific constants
|
||||||
|
pub mod sessions {
|
||||||
|
/// OpenID auth session state key
|
||||||
|
pub const OIDC_STATE_KEY: &str = "oidc-state";
|
||||||
|
/// OpenID auth remote IP address
|
||||||
|
pub const OIDC_REMOTE_IP: &str = "oidc-remote-ip";
|
||||||
|
/// Authenticated ID
|
||||||
|
pub const USER_ID: &str = "uid";
|
||||||
|
}
|
||||||
131
matrixgw_backend/src/controllers/auth_controller.rs
Normal file
131
matrixgw_backend/src/controllers/auth_controller.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::controllers::{HttpFailure, HttpResult};
|
||||||
|
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||||
|
use crate::extractors::session_extractor::MatrixGWSession;
|
||||||
|
use crate::users::{User, UserEmail};
|
||||||
|
use actix_remote_ip::RemoteIP;
|
||||||
|
use actix_web::{HttpResponse, web};
|
||||||
|
use light_openid::primitives::OpenIDConfig;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct StartOIDCResponse {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start OIDC authentication
|
||||||
|
pub async fn start_oidc(session: MatrixGWSession, remote_ip: RemoteIP) -> HttpResult {
|
||||||
|
let prov = AppConfig::get().openid_provider();
|
||||||
|
|
||||||
|
let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to fetch OpenID provider configuration! {e}");
|
||||||
|
return Ok(HttpResponse::InternalServerError()
|
||||||
|
.json("Failed to fetch OpenID provider configuration!"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = match session.gen_oidc_state(remote_ip.0) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to generate auth state! {e}");
|
||||||
|
return Ok(HttpResponse::InternalServerError().json("Failed to generate auth state!"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(StartOIDCResponse {
|
||||||
|
url: conf.gen_authorization_url(
|
||||||
|
prov.client_id,
|
||||||
|
&state,
|
||||||
|
&AppConfig::get().openid_provider().redirect_url,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct FinishOpenIDLoginQuery {
|
||||||
|
code: String,
|
||||||
|
state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish OIDC authentication
|
||||||
|
pub async fn finish_oidc(
|
||||||
|
session: MatrixGWSession,
|
||||||
|
remote_ip: RemoteIP,
|
||||||
|
req: web::Json<FinishOpenIDLoginQuery>,
|
||||||
|
) -> HttpResult {
|
||||||
|
if let Err(e) = session.validate_state(&req.state, remote_ip.0) {
|
||||||
|
log::error!("Failed to validate OIDC CB state! {e}");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Invalid state!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let prov = AppConfig::get().openid_provider();
|
||||||
|
|
||||||
|
let conf = OpenIDConfig::load_from_url(prov.configuration_url)
|
||||||
|
.await
|
||||||
|
.map_err(HttpFailure::OpenID)?;
|
||||||
|
|
||||||
|
let (token, _) = conf
|
||||||
|
.request_token(
|
||||||
|
prov.client_id,
|
||||||
|
prov.client_secret,
|
||||||
|
&req.code,
|
||||||
|
&AppConfig::get().openid_provider().redirect_url,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(HttpFailure::OpenID)?;
|
||||||
|
let (user_info, _) = conf
|
||||||
|
.request_user_info(&token)
|
||||||
|
.await
|
||||||
|
.map_err(HttpFailure::OpenID)?;
|
||||||
|
|
||||||
|
if user_info.email_verified != Some(true) {
|
||||||
|
log::error!("Email is not verified!");
|
||||||
|
return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mail = match user_info.email {
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_name = user_info.name.unwrap_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
user_info.given_name.as_deref().unwrap_or(""),
|
||||||
|
user_info.family_name.as_deref().unwrap_or("")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let user = User::create_or_update_user(&UserEmail(mail), &user_name).await?;
|
||||||
|
|
||||||
|
session.set_user(&user)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current user information
|
||||||
|
pub async fn auth_info(auth: AuthExtractor) -> HttpResult {
|
||||||
|
Ok(HttpResponse::Ok().json(auth.user))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign out user
|
||||||
|
pub async fn sign_out(auth: AuthExtractor, session: MatrixGWSession) -> HttpResult {
|
||||||
|
match auth.method {
|
||||||
|
AuthenticatedMethod::Cookie => {
|
||||||
|
session.unset_current_user()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticatedMethod::Token(token) => {
|
||||||
|
token.delete(&auth.user.email).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticatedMethod::Dev => {
|
||||||
|
// Nothing to be done, user is always authenticated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().finish())
|
||||||
|
}
|
||||||
@@ -1 +1,34 @@
|
|||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
pub mod auth_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum HttpFailure {
|
||||||
|
#[error("this resource requires higher privileges")]
|
||||||
|
Forbidden,
|
||||||
|
#[error("this resource was not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("an unspecified open id error occurred: {0}")]
|
||||||
|
OpenID(Box<dyn Error>),
|
||||||
|
#[error("an unspecified internal error occurred: {0}")]
|
||||||
|
InternalError(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for HttpFailure {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match &self {
|
||||||
|
Self::Forbidden => StatusCode::FORBIDDEN,
|
||||||
|
Self::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponse::build(self.status_code()).body(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type HttpResult = Result<HttpResponse, HttpFailure>;
|
||||||
|
|||||||
305
matrixgw_backend/src/extractors/auth_extractor.rs
Normal file
305
matrixgw_backend/src/extractors/auth_extractor.rs
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::extractors::session_extractor::MatrixGWSession;
|
||||||
|
use crate::users::{APIToken, APITokenID, User, UserEmail};
|
||||||
|
use crate::utils::time_utils::time_secs;
|
||||||
|
use actix_remote_ip::RemoteIP;
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::error::ErrorPreconditionFailed;
|
||||||
|
use actix_web::{FromRequest, HttpRequest};
|
||||||
|
use anyhow::Context;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use jwt_simple::common::VerificationOptions;
|
||||||
|
use jwt_simple::prelude::{Duration, HS256Key, MACLike};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AuthenticatedMethod {
|
||||||
|
/// User is authenticated using a cookie
|
||||||
|
Cookie,
|
||||||
|
/// User is authenticated through command line, for debugging purposes only
|
||||||
|
Dev,
|
||||||
|
/// User is authenticated using an API token
|
||||||
|
Token(APIToken),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthExtractor {
|
||||||
|
pub user: User,
|
||||||
|
pub method: AuthenticatedMethod,
|
||||||
|
pub payload: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct MatrixJWTKID {
|
||||||
|
pub user_email: UserEmail,
|
||||||
|
pub id: APITokenID,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MatrixJWTKID {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}#{}", self.user_email.0, self.id.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MatrixJWTKID {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (mail, token_id) = s
|
||||||
|
.split_once("#")
|
||||||
|
.context("Failed to decode KID in two parts!")?;
|
||||||
|
|
||||||
|
let mail = UserEmail(mail.to_string());
|
||||||
|
|
||||||
|
if !mail.is_valid() {
|
||||||
|
anyhow::bail!("Given email is invalid!")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
user_email: mail,
|
||||||
|
id: token_id.parse().context("Failed to parse API token ID")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TokenClaims {
|
||||||
|
#[serde(rename = "met")]
|
||||||
|
pub method: String,
|
||||||
|
pub uri: String,
|
||||||
|
#[serde(rename = "pay", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub payload_sha256: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthExtractor {
|
||||||
|
async fn extract_auth(
|
||||||
|
req: &HttpRequest,
|
||||||
|
remote_ip: IpAddr,
|
||||||
|
payload_bytes: Option<Bytes>,
|
||||||
|
) -> Result<Self, actix_web::Error> {
|
||||||
|
// Check for authentication using API token
|
||||||
|
if let Some(token) = req.headers().get(constants::API_AUTH_HEADER) {
|
||||||
|
let Ok(jwt_token) = token.to_str() else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Failed to decode token as string!",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to decode JWT header metadata! {e}");
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Failed to decode JWT header metadata!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract token ID
|
||||||
|
let Some(kid) = metadata.key_id() else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Missing key id in request!",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let jwt_kid = match MatrixJWTKID::from_str(kid) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to parse token id! {e}");
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Failed to parse token id!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get token information
|
||||||
|
let Ok(mut token) = APIToken::load(&jwt_kid.user_email, &jwt_kid.id).await else {
|
||||||
|
log::error!("Token not found!");
|
||||||
|
return Err(actix_web::error::ErrorForbidden("Token not found!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decode JWT
|
||||||
|
let key = HS256Key::from_bytes(token.secret.as_ref());
|
||||||
|
let verif = VerificationOptions {
|
||||||
|
max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let claims = match key.verify_token::<TokenClaims>(jwt_token, Some(verif)) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("JWT validation failed! {e}");
|
||||||
|
return Err(actix_web::error::ErrorForbidden("JWT validation failed!"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for nonce
|
||||||
|
if claims.nonce.is_none() {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"A nonce is required in auth JWT!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check IP restriction
|
||||||
|
if let Some(net) = token.network
|
||||||
|
&& !net.contains(&remote_ip)
|
||||||
|
{
|
||||||
|
log::error!(
|
||||||
|
"Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
|
||||||
|
token.id
|
||||||
|
);
|
||||||
|
return Err(actix_web::error::ErrorForbidden(
|
||||||
|
"This token cannot be used from this IP address!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for write access
|
||||||
|
if token.read_only && !req.method().is_safe() {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Read only token cannot perform write operations!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user information
|
||||||
|
let Ok(user) = User::get_by_mail(&jwt_kid.user_email).await else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Failed to get user information from token!",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update last use (if needed)
|
||||||
|
if token.shall_update_time_used() {
|
||||||
|
token.last_used = time_secs();
|
||||||
|
if let Err(e) = token.write(&jwt_kid.user_email).await {
|
||||||
|
log::error!("Failed to refresh last usage of token! {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tokens expiration
|
||||||
|
if token.is_expired() {
|
||||||
|
log::error!("Attempted to use expired token! {token:?}");
|
||||||
|
return Err(actix_web::error::ErrorBadRequest("Token has expired!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check payload
|
||||||
|
let payload = match (payload_bytes, claims.custom.payload_sha256) {
|
||||||
|
(None, _) => None,
|
||||||
|
(Some(_), None) => {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"A payload digest must be included in the JWT when the request has a payload!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(Some(payload), Some(provided_digest)) => {
|
||||||
|
let computed_digest = base16ct::lower::encode_string(&Sha256::digest(&payload));
|
||||||
|
if computed_digest != provided_digest {
|
||||||
|
log::error!(
|
||||||
|
"Expected digest {provided_digest} for payload but computed {computed_digest}!"
|
||||||
|
);
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"Computed digest is different from the one provided in the JWT!",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(payload.to_vec())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(Self {
|
||||||
|
method: AuthenticatedMethod::Token(token),
|
||||||
|
user,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if login is hard-coded as program argument
|
||||||
|
if let Some(email) = &AppConfig::get().unsecure_auto_login_email() {
|
||||||
|
let user = User::get_by_mail(email).await.map_err(|e| {
|
||||||
|
log::error!("Failed to retrieve dev user: {e}");
|
||||||
|
ErrorPreconditionFailed("Unable to retrieve dev user!")
|
||||||
|
})?;
|
||||||
|
return Ok(Self {
|
||||||
|
method: AuthenticatedMethod::Dev,
|
||||||
|
user,
|
||||||
|
payload: payload_bytes.map(|bytes| bytes.to_vec()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cookie authentication
|
||||||
|
let session = MatrixGWSession::extract(req).await?;
|
||||||
|
if let Some(mail) = session.current_user().map_err(|e| {
|
||||||
|
log::error!("Failed to retrieve user id: {e}");
|
||||||
|
ErrorPreconditionFailed("Failed to read session information!")
|
||||||
|
})? {
|
||||||
|
let user = User::get_by_mail(&mail).await.map_err(|e| {
|
||||||
|
log::error!("Failed to retrieve user from cookie session: {e}");
|
||||||
|
ErrorPreconditionFailed("Failed to retrieve user information!")
|
||||||
|
})?;
|
||||||
|
return Ok(Self {
|
||||||
|
method: AuthenticatedMethod::Cookie,
|
||||||
|
user,
|
||||||
|
payload: payload_bytes.map(|bytes| bytes.to_vec()),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Err(ErrorPreconditionFailed("Authentication required!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for AuthExtractor {
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||||
|
let req = req.clone();
|
||||||
|
|
||||||
|
let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() {
|
||||||
|
Ok(ip) => ip,
|
||||||
|
Err(e) => return Box::pin(async { Err(e) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut payload = payload.take();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let payload_bytes = match Bytes::from_request(&req, &mut payload).await {
|
||||||
|
Ok(b) => {
|
||||||
|
if b.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to extract request payload! {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::extract_auth(&req, remote_ip.0, payload_bytes).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::extractors::auth_extractor::MatrixJWTKID;
|
||||||
|
use crate::users::{APITokenID, UserEmail};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode_jwt_kid() {
|
||||||
|
let src = MatrixJWTKID {
|
||||||
|
user_email: UserEmail("test@mail.com".to_string()),
|
||||||
|
id: APITokenID::default(),
|
||||||
|
};
|
||||||
|
let encoded = src.to_string();
|
||||||
|
let decoded = encoded.parse::<MatrixJWTKID>().unwrap();
|
||||||
|
assert_eq!(src, decoded);
|
||||||
|
|
||||||
|
MatrixJWTKID::from_str("bad").unwrap_err();
|
||||||
|
MatrixJWTKID::from_str("ba#d").unwrap_err();
|
||||||
|
MatrixJWTKID::from_str("test@valid.com#d").unwrap_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
matrixgw_backend/src/extractors/mod.rs
Normal file
2
matrixgw_backend/src/extractors/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod auth_extractor;
|
||||||
|
pub mod session_extractor;
|
||||||
91
matrixgw_backend/src/extractors/session_extractor.rs
Normal file
91
matrixgw_backend/src/extractors/session_extractor.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use crate::constants;
|
||||||
|
use crate::users::{User, UserEmail};
|
||||||
|
use crate::utils::rand_utils::rand_string;
|
||||||
|
use actix_session::Session;
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::{Error, FromRequest, HttpRequest};
|
||||||
|
use futures_util::future::{Ready, ready};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
/// Matrix Gateway session errors
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum MatrixGWSessionError {
|
||||||
|
#[error("Missing state!")]
|
||||||
|
OIDCMissingState,
|
||||||
|
#[error("Missing IP address!")]
|
||||||
|
OIDCMissingIP,
|
||||||
|
#[error("Invalid state!")]
|
||||||
|
OIDCInvalidState,
|
||||||
|
#[error("Invalid IP address!")]
|
||||||
|
OIDCInvalidIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matrix Gateway session
|
||||||
|
///
|
||||||
|
/// Basic wrapper around actix-session extractor
|
||||||
|
pub struct MatrixGWSession(Session);
|
||||||
|
|
||||||
|
impl MatrixGWSession {
|
||||||
|
/// Generate OpenID state for this session
|
||||||
|
pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result<String> {
|
||||||
|
let random_string = rand_string(50);
|
||||||
|
self.0
|
||||||
|
.insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?;
|
||||||
|
self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?;
|
||||||
|
Ok(random_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate OpenID state
|
||||||
|
pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> {
|
||||||
|
let session_state: String = self
|
||||||
|
.0
|
||||||
|
.get(constants::sessions::OIDC_STATE_KEY)?
|
||||||
|
.ok_or(MatrixGWSessionError::OIDCMissingState)?;
|
||||||
|
|
||||||
|
let session_ip: IpAddr = self
|
||||||
|
.0
|
||||||
|
.get(constants::sessions::OIDC_REMOTE_IP)?
|
||||||
|
.ok_or(MatrixGWSessionError::OIDCMissingIP)?;
|
||||||
|
|
||||||
|
if session_state != state {
|
||||||
|
return Err(anyhow::anyhow!(MatrixGWSessionError::OIDCInvalidState));
|
||||||
|
}
|
||||||
|
|
||||||
|
if session_ip != ip {
|
||||||
|
return Err(anyhow::anyhow!(MatrixGWSessionError::OIDCInvalidIP));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set current user
|
||||||
|
pub fn set_user(&self, user: &User) -> anyhow::Result<()> {
|
||||||
|
self.0.insert(constants::sessions::USER_ID, &user.email)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current user
|
||||||
|
pub fn current_user(&self) -> anyhow::Result<Option<UserEmail>> {
|
||||||
|
Ok(self.0.get(constants::sessions::USER_ID)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove defined user
|
||||||
|
pub fn unset_current_user(&self) -> anyhow::Result<()> {
|
||||||
|
self.0.remove(constants::sessions::USER_ID);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for MatrixGWSession {
|
||||||
|
type Error = Error;
|
||||||
|
type Future = Ready<Result<Self, Error>>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||||
|
ready(
|
||||||
|
Session::from_request(req, &mut Payload::None)
|
||||||
|
.into_inner()
|
||||||
|
.map(MatrixGWSession),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
pub mod app_config;
|
pub mod app_config;
|
||||||
|
pub mod constants;
|
||||||
pub mod controllers;
|
pub mod controllers;
|
||||||
|
pub mod extractors;
|
||||||
|
pub mod users;
|
||||||
|
pub mod utils;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use actix_session::storage::RedisSessionStore;
|
|||||||
use actix_web::cookie::Key;
|
use actix_web::cookie::Key;
|
||||||
use actix_web::{App, HttpServer, web};
|
use actix_web::{App, HttpServer, web};
|
||||||
use matrixgw_backend::app_config::AppConfig;
|
use matrixgw_backend::app_config::AppConfig;
|
||||||
use matrixgw_backend::controllers::server_controller;
|
use matrixgw_backend::controllers::{auth_controller, server_controller};
|
||||||
|
use matrixgw_backend::users::User;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
@@ -17,6 +18,13 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to connect to Redis!");
|
.expect("Failed to connect to Redis!");
|
||||||
|
|
||||||
|
// Auto create default account, if requested
|
||||||
|
if let Some(mail) = &AppConfig::get().unsecure_auto_login_email() {
|
||||||
|
User::create_or_update_user(mail, "Anonymous")
|
||||||
|
.await
|
||||||
|
.expect("Failed to create auto-login account!");
|
||||||
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Starting to listen on {} for {}",
|
"Starting to listen on {} for {}",
|
||||||
AppConfig::get().listen_address,
|
AppConfig::get().listen_address,
|
||||||
@@ -40,6 +48,20 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/server/config",
|
"/api/server/config",
|
||||||
web::get().to(server_controller::config),
|
web::get().to(server_controller::config),
|
||||||
)
|
)
|
||||||
|
// Auth controller
|
||||||
|
.route(
|
||||||
|
"/api/auth/start_oidc",
|
||||||
|
web::get().to(auth_controller::start_oidc),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/auth/finish_oidc",
|
||||||
|
web::post().to(auth_controller::finish_oidc),
|
||||||
|
)
|
||||||
|
.route("/api/auth/info", web::get().to(auth_controller::auth_info))
|
||||||
|
.route(
|
||||||
|
"/api/auth/sign_out",
|
||||||
|
web::get().to(auth_controller::sign_out),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.workers(4)
|
.workers(4)
|
||||||
.bind(&AppConfig::get().listen_address)?
|
.bind(&AppConfig::get().listen_address)?
|
||||||
|
|||||||
168
matrixgw_backend/src/users.rs
Normal file
168
matrixgw_backend/src/users.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::utils::time_utils::time_secs;
|
||||||
|
use jwt_simple::reexports::serde_json;
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Matrix Gateway user errors
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum MatrixGWUserError {
|
||||||
|
#[error("Failed to load user metadata: {0}")]
|
||||||
|
LoadUserMetadata(std::io::Error),
|
||||||
|
#[error("Failed to decode user metadata: {0}")]
|
||||||
|
DecodeUserMetadata(serde_json::Error),
|
||||||
|
#[error("Failed to save user metadata: {0}")]
|
||||||
|
SaveUserMetadata(std::io::Error),
|
||||||
|
#[error("Failed to delete API token: {0}")]
|
||||||
|
DeleteToken(std::io::Error),
|
||||||
|
#[error("Failed to load API token: {0}")]
|
||||||
|
LoadApiToken(std::io::Error),
|
||||||
|
#[error("Failed to decode API token: {0}")]
|
||||||
|
DecodeApiToken(serde_json::Error),
|
||||||
|
#[error("API Token does not exists!")]
|
||||||
|
ApiTokenDoesNotExists,
|
||||||
|
#[error("Failed to save API token: {0}")]
|
||||||
|
SaveAPIToken(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct UserEmail(pub String);
|
||||||
|
|
||||||
|
impl UserEmail {
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
mailchecker::is_valid(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct APITokenID(pub uuid::Uuid);
|
||||||
|
|
||||||
|
impl Default for APITokenID {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(uuid::Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for APITokenID {
|
||||||
|
type Err = uuid::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(uuid::Uuid::from_str(s)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub email: UserEmail,
|
||||||
|
pub name: String,
|
||||||
|
pub time_create: u64,
|
||||||
|
pub last_login: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
/// Get a user by its mail
|
||||||
|
pub async fn get_by_mail(mail: &UserEmail) -> anyhow::Result<Self> {
|
||||||
|
let path = AppConfig::get().user_metadata_file_path(mail);
|
||||||
|
let data = std::fs::read_to_string(path).map_err(MatrixGWUserError::LoadUserMetadata)?;
|
||||||
|
Ok(serde_json::from_str(&data).map_err(MatrixGWUserError::DecodeUserMetadata)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update user metadata on disk
|
||||||
|
pub async fn write(&self) -> anyhow::Result<()> {
|
||||||
|
let path = AppConfig::get().user_metadata_file_path(&self.email);
|
||||||
|
std::fs::write(&path, serde_json::to_string(&self)?)
|
||||||
|
.map_err(MatrixGWUserError::SaveUserMetadata)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create or update user information
|
||||||
|
pub async fn create_or_update_user(mail: &UserEmail, name: &str) -> anyhow::Result<User> {
|
||||||
|
let storage_dir = AppConfig::get().user_directory(mail);
|
||||||
|
let mut user = if !storage_dir.exists() {
|
||||||
|
std::fs::create_dir_all(storage_dir)?;
|
||||||
|
|
||||||
|
User {
|
||||||
|
email: mail.clone(),
|
||||||
|
name: name.to_string(),
|
||||||
|
time_create: time_secs(),
|
||||||
|
last_login: time_secs(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::get_by_mail(mail).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update some user information
|
||||||
|
user.name = name.to_string();
|
||||||
|
user.last_login = time_secs();
|
||||||
|
user.write().await?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single API client information
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct APIToken {
|
||||||
|
/// Token unique ID
|
||||||
|
pub id: APITokenID,
|
||||||
|
|
||||||
|
/// Client description
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
/// Restricted API network for token
|
||||||
|
pub network: Option<ipnet::IpNet>,
|
||||||
|
|
||||||
|
/// Client secret
|
||||||
|
pub secret: String,
|
||||||
|
|
||||||
|
/// Client creation time
|
||||||
|
pub created: u64,
|
||||||
|
|
||||||
|
/// Client last usage time
|
||||||
|
pub last_used: u64,
|
||||||
|
|
||||||
|
/// Read only access
|
||||||
|
pub read_only: bool,
|
||||||
|
|
||||||
|
/// Token max inactivity
|
||||||
|
pub max_inactivity: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl APIToken {
|
||||||
|
/// Get a token information
|
||||||
|
pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result<Self> {
|
||||||
|
let token_file = AppConfig::get().user_api_token_metadata_file(email, id);
|
||||||
|
match token_file.exists() {
|
||||||
|
true => Ok(serde_json::from_str::<Self>(
|
||||||
|
&std::fs::read_to_string(&token_file).map_err(MatrixGWUserError::LoadApiToken)?,
|
||||||
|
)
|
||||||
|
.map_err(MatrixGWUserError::DecodeApiToken)?),
|
||||||
|
false => Err(MatrixGWUserError::ApiTokenDoesNotExists.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write this token information
|
||||||
|
pub async fn write(&self, mail: &UserEmail) -> anyhow::Result<()> {
|
||||||
|
let path = AppConfig::get().user_api_token_metadata_file(mail, &self.id);
|
||||||
|
std::fs::write(&path, serde_json::to_string(&self)?)
|
||||||
|
.map_err(MatrixGWUserError::SaveAPIToken)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete this token
|
||||||
|
pub async fn delete(self, email: &UserEmail) -> anyhow::Result<()> {
|
||||||
|
let token_file = AppConfig::get().user_api_token_metadata_file(email, &self.id);
|
||||||
|
std::fs::remove_file(&token_file).map_err(MatrixGWUserError::DeleteToken)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shall_update_time_used(&self) -> bool {
|
||||||
|
let refresh_interval = min(600, self.max_inactivity / 10);
|
||||||
|
|
||||||
|
(self.last_used) < time_secs() - refresh_interval
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_expired(&self) -> bool {
|
||||||
|
(self.last_used + self.max_inactivity) < time_secs()
|
||||||
|
}
|
||||||
|
}
|
||||||
6
matrixgw_backend/src/utils/crypt_utils.rs
Normal file
6
matrixgw_backend/src/utils/crypt_utils.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
/// Compute SHA256sum of a given string
|
||||||
|
pub fn sha256str(input: &str) -> String {
|
||||||
|
hex::encode(Sha256::digest(input.as_bytes()))
|
||||||
|
}
|
||||||
3
matrixgw_backend/src/utils/mod.rs
Normal file
3
matrixgw_backend/src/utils/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod crypt_utils;
|
||||||
|
pub mod rand_utils;
|
||||||
|
pub mod time_utils;
|
||||||
6
matrixgw_backend/src/utils/rand_utils.rs
Normal file
6
matrixgw_backend/src/utils/rand_utils.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use rand::distr::{Alphanumeric, SampleString};
|
||||||
|
|
||||||
|
/// Generate a random string of a given length
|
||||||
|
pub fn rand_string(len: usize) -> String {
|
||||||
|
Alphanumeric.sample_string(&mut rand::rng(), len)
|
||||||
|
}
|
||||||
9
matrixgw_backend/src/utils/time_utils.rs
Normal file
9
matrixgw_backend/src/utils/time_utils.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
/// Get the current time since epoch
|
||||||
|
pub fn time_secs() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user