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"
|
||||
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]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3012,6 +3035,7 @@ dependencies = [
|
||||
"hex",
|
||||
"ipnet",
|
||||
"jwt-simple",
|
||||
"lazy-regex",
|
||||
"lazy_static",
|
||||
"light-openid",
|
||||
"log",
|
||||
|
||||
@@ -32,3 +32,4 @@ matrix-sdk = "0.14.0"
|
||||
url = "2.5.7"
|
||||
ractor = "0.15.9"
|
||||
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
|
||||
pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60;
|
||||
|
||||
/// Length of generated tokens
|
||||
pub const TOKENS_LEN: usize = 50;
|
||||
|
||||
/// Session-specific constants
|
||||
pub mod sessions {
|
||||
/// OpenID auth session state key
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::error::Error;
|
||||
pub mod auth_controller;
|
||||
pub mod matrix_link_controller;
|
||||
pub mod server_controller;
|
||||
pub mod tokens_controller;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
impl AsRef<User> for AuthExtractor {
|
||||
fn as_ref(&self) -> &User {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthExtractor {
|
||||
pub fn decode_json_body<E: DeserializeOwned + Send>(&self) -> anyhow::Result<E> {
|
||||
let payload = self
|
||||
@@ -156,8 +162,9 @@ impl AuthExtractor {
|
||||
}
|
||||
|
||||
// Check IP restriction
|
||||
if let Some(net) = token.network
|
||||
&& !net.contains(&remote_ip)
|
||||
if let Some(nets) = &token.base.networks
|
||||
&& !nets.is_empty()
|
||||
&& !nets.iter().any(|n| n.contains(&remote_ip))
|
||||
{
|
||||
log::error!(
|
||||
"Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
|
||||
@@ -169,7 +176,7 @@ impl AuthExtractor {
|
||||
}
|
||||
|
||||
// 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(
|
||||
"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::broadcast_messages::BroadcastMessage;
|
||||
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::users::User;
|
||||
use ractor::Actor;
|
||||
@@ -110,6 +112,9 @@ async fn main() -> std::io::Result<()> {
|
||||
"/api/matrix_link/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)
|
||||
.bind(&AppConfig::get().listen_address)?
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::constants;
|
||||
use crate::controllers::server_controller::ServerConstraints;
|
||||
use crate::matrix_connection::matrix_client::EncryptionRecoveryState;
|
||||
use crate::utils::rand_utils::rand_string;
|
||||
use crate::utils::time_utils::time_secs;
|
||||
use anyhow::Context;
|
||||
use jwt_simple::reexports::serde_json;
|
||||
use std::cmp::min;
|
||||
use std::str::FromStr;
|
||||
@@ -14,6 +18,8 @@ enum MatrixGWUserError {
|
||||
DecodeUserMetadata(serde_json::Error),
|
||||
#[error("Failed to save user metadata: {0}")]
|
||||
SaveUserMetadata(std::io::Error),
|
||||
#[error("Failed to create API token directory: {0}")]
|
||||
CreateApiTokensDirectory(std::io::Error),
|
||||
#[error("Failed to delete API token: {0}")]
|
||||
DeleteToken(std::io::Error),
|
||||
#[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)]
|
||||
pub struct APIToken {
|
||||
/// Token unique ID
|
||||
pub id: APITokenID,
|
||||
|
||||
/// Client description
|
||||
pub description: String,
|
||||
pub struct BaseAPIToken {
|
||||
/// Token name
|
||||
pub name: String,
|
||||
|
||||
/// 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
|
||||
pub secret: String,
|
||||
@@ -121,15 +173,58 @@ pub struct APIToken {
|
||||
|
||||
/// 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 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
|
||||
pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result<Self> {
|
||||
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 {
|
||||
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 {
|
||||
(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