Create & list tokens
This commit is contained in:
24
matrixgw_backend/Cargo.lock
generated
24
matrixgw_backend/Cargo.lock
generated
@@ -2530,6 +2530,29 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-regex"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29"
|
||||||
|
dependencies = [
|
||||||
|
"lazy-regex-proc_macros",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-regex-proc_macros"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -3012,6 +3035,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"jwt-simple",
|
"jwt-simple",
|
||||||
|
"lazy-regex",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"light-openid",
|
"light-openid",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -31,4 +31,5 @@ mailchecker = "6.0.19"
|
|||||||
matrix-sdk = "0.14.0"
|
matrix-sdk = "0.14.0"
|
||||||
url = "2.5.7"
|
url = "2.5.7"
|
||||||
ractor = "0.15.9"
|
ractor = "0.15.9"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
lazy-regex = "3.4.2"
|
||||||
@@ -4,6 +4,9 @@ pub const API_AUTH_HEADER: &str = "x-client-auth";
|
|||||||
/// Max token validity, in seconds
|
/// Max token validity, in seconds
|
||||||
pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60;
|
pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60;
|
||||||
|
|
||||||
|
/// Length of generated tokens
|
||||||
|
pub const TOKENS_LEN: usize = 50;
|
||||||
|
|
||||||
/// Session-specific constants
|
/// Session-specific constants
|
||||||
pub mod sessions {
|
pub mod sessions {
|
||||||
/// OpenID auth session state key
|
/// OpenID auth session state key
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::error::Error;
|
|||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
pub mod matrix_link_controller;
|
pub mod matrix_link_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
|
pub mod tokens_controller;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum HttpFailure {
|
pub enum HttpFailure {
|
||||||
|
|||||||
36
matrixgw_backend/src/controllers/tokens_controller.rs
Normal file
36
matrixgw_backend/src/controllers/tokens_controller.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||||
|
use crate::users::{APIToken, BaseAPIToken};
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
/// Create a new token
|
||||||
|
pub async fn create(auth: AuthExtractor) -> HttpResult {
|
||||||
|
if matches!(auth.method, AuthenticatedMethod::Token(_)) {
|
||||||
|
return Ok(HttpResponse::Forbidden()
|
||||||
|
.json("It is not allowed to create a token using another token!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = auth.decode_json_body::<BaseAPIToken>()?;
|
||||||
|
|
||||||
|
if let Some(err) = base.check() {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = APIToken::create(&auth.as_ref().email, base).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of tokens of current user
|
||||||
|
pub async fn get_list(auth: AuthExtractor) -> HttpResult {
|
||||||
|
Ok(HttpResponse::Ok().json(
|
||||||
|
APIToken::list_user(&auth.as_ref().email)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut t| {
|
||||||
|
t.secret = String::new();
|
||||||
|
t
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -34,6 +34,12 @@ pub struct AuthExtractor {
|
|||||||
pub payload: Option<Vec<u8>>,
|
pub payload: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<User> for AuthExtractor {
|
||||||
|
fn as_ref(&self) -> &User {
|
||||||
|
&self.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AuthExtractor {
|
impl AuthExtractor {
|
||||||
pub fn decode_json_body<E: DeserializeOwned + Send>(&self) -> anyhow::Result<E> {
|
pub fn decode_json_body<E: DeserializeOwned + Send>(&self) -> anyhow::Result<E> {
|
||||||
let payload = self
|
let payload = self
|
||||||
@@ -156,8 +162,9 @@ impl AuthExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check IP restriction
|
// Check IP restriction
|
||||||
if let Some(net) = token.network
|
if let Some(nets) = &token.base.networks
|
||||||
&& !net.contains(&remote_ip)
|
&& !nets.is_empty()
|
||||||
|
&& !nets.iter().any(|n| n.contains(&remote_ip))
|
||||||
{
|
{
|
||||||
log::error!(
|
log::error!(
|
||||||
"Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
|
"Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
|
||||||
@@ -169,7 +176,7 @@ impl AuthExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for write access
|
// Check for write access
|
||||||
if token.read_only && !req.method().is_safe() {
|
if token.base.read_only && !req.method().is_safe() {
|
||||||
return Err(actix_web::error::ErrorBadRequest(
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
"Read only token cannot perform write operations!",
|
"Read only token cannot perform write operations!",
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ use actix_web::{App, HttpServer, web};
|
|||||||
use matrixgw_backend::app_config::AppConfig;
|
use matrixgw_backend::app_config::AppConfig;
|
||||||
use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
||||||
use matrixgw_backend::constants;
|
use matrixgw_backend::constants;
|
||||||
use matrixgw_backend::controllers::{auth_controller, matrix_link_controller, server_controller};
|
use matrixgw_backend::controllers::{
|
||||||
|
auth_controller, matrix_link_controller, server_controller, tokens_controller,
|
||||||
|
};
|
||||||
use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor;
|
use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor;
|
||||||
use matrixgw_backend::users::User;
|
use matrixgw_backend::users::User;
|
||||||
use ractor::Actor;
|
use ractor::Actor;
|
||||||
@@ -110,6 +112,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/matrix_link/set_recovery_key",
|
"/api/matrix_link/set_recovery_key",
|
||||||
web::post().to(matrix_link_controller::set_recovery_key),
|
web::post().to(matrix_link_controller::set_recovery_key),
|
||||||
)
|
)
|
||||||
|
// API Tokens controller
|
||||||
|
.route("/api/token", web::post().to(tokens_controller::create))
|
||||||
|
.route("/api/tokens", web::get().to(tokens_controller::get_list))
|
||||||
})
|
})
|
||||||
.workers(4)
|
.workers(4)
|
||||||
.bind(&AppConfig::get().listen_address)?
|
.bind(&AppConfig::get().listen_address)?
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::controllers::server_controller::ServerConstraints;
|
||||||
use crate::matrix_connection::matrix_client::EncryptionRecoveryState;
|
use crate::matrix_connection::matrix_client::EncryptionRecoveryState;
|
||||||
|
use crate::utils::rand_utils::rand_string;
|
||||||
use crate::utils::time_utils::time_secs;
|
use crate::utils::time_utils::time_secs;
|
||||||
|
use anyhow::Context;
|
||||||
use jwt_simple::reexports::serde_json;
|
use jwt_simple::reexports::serde_json;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -14,6 +18,8 @@ enum MatrixGWUserError {
|
|||||||
DecodeUserMetadata(serde_json::Error),
|
DecodeUserMetadata(serde_json::Error),
|
||||||
#[error("Failed to save user metadata: {0}")]
|
#[error("Failed to save user metadata: {0}")]
|
||||||
SaveUserMetadata(std::io::Error),
|
SaveUserMetadata(std::io::Error),
|
||||||
|
#[error("Failed to create API token directory: {0}")]
|
||||||
|
CreateApiTokensDirectory(std::io::Error),
|
||||||
#[error("Failed to delete API token: {0}")]
|
#[error("Failed to delete API token: {0}")]
|
||||||
DeleteToken(std::io::Error),
|
DeleteToken(std::io::Error),
|
||||||
#[error("Failed to load API token: {0}")]
|
#[error("Failed to load API token: {0}")]
|
||||||
@@ -101,17 +107,63 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single API client information
|
/// Base API token information
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct APIToken {
|
pub struct BaseAPIToken {
|
||||||
/// Token unique ID
|
/// Token name
|
||||||
pub id: APITokenID,
|
pub name: String,
|
||||||
|
|
||||||
/// Client description
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
/// Restricted API network for token
|
/// Restricted API network for token
|
||||||
pub network: Option<ipnet::IpNet>,
|
pub networks: Option<Vec<ipnet::IpNet>>,
|
||||||
|
|
||||||
|
/// Read only access
|
||||||
|
pub read_only: bool,
|
||||||
|
|
||||||
|
/// Token max inactivity
|
||||||
|
pub max_inactivity: u32,
|
||||||
|
|
||||||
|
/// Token expiration
|
||||||
|
pub expiration: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseAPIToken {
|
||||||
|
/// Check API token information validity
|
||||||
|
pub fn check(&self) -> Option<&'static str> {
|
||||||
|
let constraints = ServerConstraints::default();
|
||||||
|
|
||||||
|
if !lazy_regex::regex!("^[a-zA-Z0-9 :-]+$").is_match(&self.name) {
|
||||||
|
return Some("Token name contains invalid characters!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints.token_name.check_str(&self.name) {
|
||||||
|
return Some("Invalid token name length!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints
|
||||||
|
.token_max_inactivity
|
||||||
|
.check_u32(self.max_inactivity)
|
||||||
|
{
|
||||||
|
return Some("Invalid token max inactivity!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expiration) = self.expiration
|
||||||
|
&& expiration <= time_secs()
|
||||||
|
{
|
||||||
|
return Some("Given expiration time is in the past!");
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single API token information
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct APIToken {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub base: BaseAPIToken,
|
||||||
|
|
||||||
|
/// Token unique ID
|
||||||
|
pub id: APITokenID,
|
||||||
|
|
||||||
/// Client secret
|
/// Client secret
|
||||||
pub secret: String,
|
pub secret: String,
|
||||||
@@ -121,15 +173,58 @@ pub struct APIToken {
|
|||||||
|
|
||||||
/// Client last usage time
|
/// Client last usage time
|
||||||
pub last_used: u64,
|
pub last_used: u64,
|
||||||
|
|
||||||
/// Read only access
|
|
||||||
pub read_only: bool,
|
|
||||||
|
|
||||||
/// Token max inactivity
|
|
||||||
pub max_inactivity: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl APIToken {
|
impl APIToken {
|
||||||
|
/// Get the list of tokens of a user
|
||||||
|
pub async fn list_user(email: &UserEmail) -> anyhow::Result<Vec<Self>> {
|
||||||
|
let tokens_dir = AppConfig::get().user_api_token_directory(email);
|
||||||
|
|
||||||
|
if !tokens_dir.exists() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut list = vec![];
|
||||||
|
for u in std::fs::read_dir(&tokens_dir)? {
|
||||||
|
let entry = u?;
|
||||||
|
list.push(
|
||||||
|
Self::load(
|
||||||
|
email,
|
||||||
|
&APITokenID::from_str(
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_str()
|
||||||
|
.context("Cannot decode API Token ID as string!")?,
|
||||||
|
)?,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new token
|
||||||
|
pub async fn create(email: &UserEmail, base: BaseAPIToken) -> anyhow::Result<Self> {
|
||||||
|
let tokens_dir = AppConfig::get().user_api_token_directory(email);
|
||||||
|
|
||||||
|
if !tokens_dir.exists() {
|
||||||
|
std::fs::create_dir_all(tokens_dir)
|
||||||
|
.map_err(MatrixGWUserError::CreateApiTokensDirectory)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = APIToken {
|
||||||
|
base,
|
||||||
|
id: Default::default(),
|
||||||
|
secret: rand_string(constants::TOKENS_LEN),
|
||||||
|
created: time_secs(),
|
||||||
|
last_used: time_secs(),
|
||||||
|
};
|
||||||
|
|
||||||
|
token.write(email).await?;
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a token information
|
/// Get a token information
|
||||||
pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result<Self> {
|
pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result<Self> {
|
||||||
let token_file = AppConfig::get().user_api_token_metadata_file(email, id);
|
let token_file = AppConfig::get().user_api_token_metadata_file(email, id);
|
||||||
@@ -158,13 +253,21 @@ impl APIToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn shall_update_time_used(&self) -> bool {
|
pub fn shall_update_time_used(&self) -> bool {
|
||||||
let refresh_interval = min(600, self.max_inactivity / 10);
|
let refresh_interval = min(600, self.base.max_inactivity / 10);
|
||||||
|
|
||||||
(self.last_used) < time_secs() - refresh_interval
|
(self.last_used) < time_secs() - refresh_interval as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
(self.last_used + self.max_inactivity) < time_secs()
|
// Check for hard coded expiration
|
||||||
|
if let Some(exp_time) = self.base.expiration
|
||||||
|
&& exp_time < time_secs()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control max token inactivity
|
||||||
|
(self.last_used + self.base.max_inactivity as u64) < time_secs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user