This commit is contained in:
root
2025-12-14 10:39:18 +03:00
commit 639f4e2b4e
179 changed files with 21065 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
use crate::error::WebError;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::convert::TryInto;
use std::fs;
use std::path::{Path, PathBuf};
use ticktalk_db::{models::NewUser, repositories::UserRepository};
use ticktalk_types::{LoginRequest, UserResponse};
use tokio::process::Command;
use tracing::debug;
#[derive(Clone)]
pub struct AuthService {
user_repo: UserRepository,
}
impl AuthService {
pub fn new(user_repo: UserRepository) -> Self {
Self { user_repo }
}
pub async fn login(&self, creds: LoginRequest) -> Result<UserResponse, WebError> {
let ticket_bytes = STANDARD
.decode(creds.ticket.as_bytes())
.map_err(|_| WebError::BadRequest("Invalid ticket payload".into()))?;
let parsed: ParsedTicket = ParsedTicket::from_bytes(&ticket_bytes)?;
let username = normalize_username(&parsed.client_principal);
let ticket_cache_path = format!("/tmp/user_{}.cache", username);
let cache_exists = Path::new(&ticket_cache_path).exists();
if !cache_exists {
debug!(
target: "auth::ticket",
"No cache for {}, will persist received ticket",
username
);
fs::write(&ticket_cache_path, &ticket_bytes)
.map_err(|e| WebError::InternalServerError(format!("Failed to persist ticket cache: {e}")))?;
}
Self::login_via_kerberos(
&ticket_cache_path,
&parsed.client_principal,
&normalize_realm(&parsed.service_principal),
)
.await?;
if let Some(user) = self
.user_repo
.find_by_username(username.clone())
.await?
{
return Ok(UserResponse {
id: user.id,
username,
});
}
let created = self
.user_repo
.create(NewUser {
username,
})
.await?;
Ok(UserResponse {
id: created.id,
username: created.username,
})
}
async fn login_via_kerberos(tgs_path: &str, principal: &str, realm: &str) -> Result<(), WebError> {
let service_principal = format!("service@{}", realm);
debug!(
target: "auth::kerberos",
"Invoking kauth --cache {tgs_path} {principal} {service_principal} kdc"
);
let output = Command::new("kauth")
.arg("--cache")
.arg(tgs_path)
.arg(principal)
.arg(&service_principal)
.arg("kdc")
.output()
.await
.map_err(|e| WebError::InternalServerError(format!("Kerberos exec failed: {e}")))?;
debug!(
target: "auth::kerberos",
status = %output.status,
"Kerberos helper finished"
);
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if stdout.contains("OK") {
return Ok(());
} else if stdout.contains("FAIL") {
return Err(WebError::Unauthorized)
}
let stderr = String::from_utf8_lossy(&output.stderr);
Err(WebError::InternalServerError(format!(
"Kerberos rejected {} via {}: {}",
principal, service_principal, stderr.trim()
)))
}
}
fn normalize_username(principal: &str) -> String {
principal.split('@').next().unwrap_or(principal).to_string()
}
fn normalize_realm(principal: &str) -> String {
principal
.split('@')
.nth(1)
.map(|realm| realm.trim().to_ascii_uppercase())
.filter(|realm| !realm.is_empty())
.unwrap_or_else(|| "TICKTALK.LOCAL".to_string())
}
fn ticket_cache_dir() -> PathBuf {
PathBuf::from("/tmp/ticktalk-cache")
}
fn ticket_cache_path(username: &str) -> PathBuf {
ticket_cache_dir().join(format!("user_{}.cache", username))
}
struct ParsedTicket {
ticket_id: String,
client_principal: String,
service_principal: String,
realm: String,
expires_at: u64,
forwardable: bool,
is_tgt: bool,
ticket_blob: Vec<u8>,
client_key: String,
}
impl ParsedTicket {
fn from_bytes(data: &[u8]) -> Result<Self, WebError> {
let mut offset = 0usize;
fn ensure(data: &[u8], offset: usize, needed: usize) -> Result<(), WebError> {
if offset + needed > data.len() {
return Err(WebError::BadRequest("Ticket blob truncated".into()));
}
Ok(())
}
fn read_u32(data: &[u8], offset: &mut usize) -> Result<u32, WebError> {
ensure(data, *offset, 4)?;
let value = u32::from_be_bytes(data[*offset..*offset + 4].try_into().unwrap());
*offset += 4;
Ok(value)
}
fn read_u64(data: &[u8], offset: &mut usize) -> Result<u64, WebError> {
ensure(data, *offset, 8)?;
let value = u64::from_be_bytes(data[*offset..*offset + 8].try_into().unwrap());
*offset += 8;
Ok(value)
}
fn read_string(data: &[u8], offset: &mut usize) -> Result<String, WebError> {
let len = read_u32(data, offset)? as usize;
ensure(data, *offset, len)?;
let slice = &data[*offset..*offset + len];
*offset += len;
String::from_utf8(slice.to_vec())
.map_err(|_| WebError::BadRequest("Ticket string is not UTF-8".into()))
}
let ticket_id = read_string(data, &mut offset)?;
let service_principal = read_string(data, &mut offset)?;
let client_principal = read_string(data, &mut offset)?;
let realm = read_string(data, &mut offset)?;
let expires_at = read_u64(data, &mut offset)?;
ensure(data, offset, 2)?;
let forwardable = data[offset] != 0;
offset += 1;
let is_tgt = data[offset] != 0;
offset += 1;
let ticket_blob_len = read_u32(data, &mut offset)? as usize;
ensure(data, offset, ticket_blob_len)?;
let ticket_blob = data[offset..offset + ticket_blob_len].to_vec();
offset += ticket_blob_len;
let client_key = read_string(data, &mut offset)?;
Ok(Self {
ticket_id,
client_principal,
service_principal,
realm,
expires_at,
forwardable,
is_tgt,
ticket_blob,
client_key,
})
}
}

View File

@@ -0,0 +1,91 @@
use crate::error::WebError;
use ticktalk_db::models::{Chat, Message, NewChat, NewMessage};
use ticktalk_db::repositories::{ChatRepository, MessageRepository};
use ticktalk_types::*;
use uuid::Uuid;
#[derive(Clone)]
pub struct ChatService {
chat_repo: ChatRepository,
message_repo: MessageRepository,
}
impl ChatService {
pub fn new(chat_repo: ChatRepository, message_repo: MessageRepository) -> Self {
Self {
chat_repo,
message_repo,
}
}
pub async fn get_chat(&self, chat_id: Uuid) -> Result<ChatResponse, WebError> {
match self.chat_repo.find_by_id(chat_id).await {
Ok(Some(chat)) => Ok(ChatResponse {
id: chat.id,
first_user_id: chat.first_user_id,
second_user_id: chat.second_user_id,
}),
Ok(None) => Err(WebError::NotFound),
Err(err) => Err(WebError::DatabaseError(err)),
}
}
pub async fn create_chat(&self, chat_req: CreateChatRequest) -> Result<ChatResponse, WebError> {
let new_chat = NewChat {
first_user_id: chat_req.first_user_id,
second_user_id: chat_req.second_user_id,
};
let chat = self.chat_repo.create(new_chat).await?;
Ok(chat_into_response(chat))
}
pub async fn get_chat_messages(&self, chat_id: Uuid) -> Result<Vec<MessageResponse>, WebError> {
let messages = self
.message_repo
.find_by_chat_id(chat_id)
.await?
.into_iter()
.map(message_into_response)
.collect::<Vec<MessageResponse>>();
Ok(messages)
}
pub async fn create_message(
&self,
message_req: CreateMessageRequest,
) -> Result<MessageResponse, WebError> {
let new_message = NewMessage {
chat_id: message_req.chat_id,
sender_id: message_req.sender_id,
recipient_id: message_req.recipient_id,
content: message_req.content,
};
let message = self.message_repo.create(new_message).await?;
Ok(message_into_response(message))
}
pub async fn get_user_chats(&self, user_id: Uuid) -> Result<Vec<ChatResponse>, WebError> {
let chats = self.chat_repo.find_by_user(user_id).await?;
Ok(chats.into_iter().map(chat_into_response).collect())
}
}
fn chat_into_response(chat: Chat) -> ChatResponse {
ChatResponse {
id: chat.id,
first_user_id: chat.first_user_id,
second_user_id: chat.second_user_id,
}
}
fn message_into_response(message: Message) -> MessageResponse {
MessageResponse {
sender_id: message.sender_id,
recipient_id: message.recipient_id,
content: message.content,
created_at: message.created_at,
}
}

View File

@@ -0,0 +1,7 @@
pub mod auth;
pub mod chat;
pub mod user;
pub use self::auth::*;
pub use self::chat::*;
pub use self::user::*;

View File

@@ -0,0 +1,38 @@
use crate::error::WebError;
use ticktalk_db::{repositories::UserRepository};
use ticktalk_types::UserResponse;
use uuid::Uuid;
#[derive(Clone)]
pub struct UserService {
user_repo: UserRepository,
}
impl UserService {
pub fn new(user_repo: UserRepository) -> Self {
Self { user_repo }
}
pub async fn get_user(&self, id: Uuid) -> Result<UserResponse, WebError> {
match self.user_repo.find_by_id(id).await? {
Some(user) => Ok(UserResponse {
id: user.id,
username: user.username,
}),
None => Err(WebError::NotFound),
}
}
pub async fn get_user_by_username(
&self,
username_query: &str,
) -> Result<UserResponse, WebError> {
match self.user_repo.find_by_username(username_query.to_string()).await? {
Some(user) => Ok(UserResponse {
id: user.id,
username: user.username,
}),
None => Err(WebError::NotFound),
}
}
}