pinakes-server: TLS support; session persistence and security polish

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If2c9c3e3af62bbf9f33a97be89ac40bc6a6a6964
This commit is contained in:
raf 2026-01-31 15:20:27 +03:00
commit 87a4482576
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
19 changed files with 1835 additions and 111 deletions

View file

@ -3,6 +3,8 @@ use std::path::PathBuf;
use chrono::Utc;
use deadpool_postgres::{Config as PoolConfig, Pool, Runtime};
use native_tls::TlsConnector;
use postgres_native_tls::MakeTlsConnector;
use tokio_postgres::types::ToSql;
use tokio_postgres::{NoTls, Row};
use uuid::Uuid;
@ -27,19 +29,72 @@ impl PostgresBackend {
pool_config.user = Some(config.username.clone());
pool_config.password = Some(config.password.clone());
let pool = pool_config
.create_pool(Some(Runtime::Tokio1), NoTls)
.map_err(|e| {
PinakesError::Database(format!("failed to create connection pool: {e}"))
if config.tls_enabled {
// Build TLS connector
let mut tls_builder = TlsConnector::builder();
// Load custom CA certificate if provided
if let Some(ref ca_cert_path) = config.tls_ca_cert_path {
let cert_bytes = std::fs::read(ca_cert_path).map_err(|e| {
PinakesError::Config(format!(
"failed to read CA certificate file {}: {e}",
ca_cert_path.display()
))
})?;
let cert = native_tls::Certificate::from_pem(&cert_bytes).map_err(|e| {
PinakesError::Config(format!(
"failed to parse CA certificate {}: {e}",
ca_cert_path.display()
))
})?;
tls_builder.add_root_certificate(cert);
}
// Configure certificate validation
if !config.tls_verify_ca {
tracing::warn!(
"PostgreSQL TLS certificate verification disabled - this is insecure!"
);
tls_builder.danger_accept_invalid_certs(true);
}
let connector = tls_builder.build().map_err(|e| {
PinakesError::Database(format!("failed to build TLS connector: {e}"))
})?;
let tls = MakeTlsConnector::new(connector);
let pool = pool_config
.create_pool(Some(Runtime::Tokio1), tls)
.map_err(|e| {
PinakesError::Database(format!("failed to create connection pool: {e}"))
})?;
// Verify connectivity
let _ = pool.get().await.map_err(|e| {
PinakesError::Database(format!("failed to connect to postgres: {e}"))
})?;
// Verify connectivity
let _ = pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("failed to connect to postgres: {e}")))?;
tracing::info!("PostgreSQL connection established with TLS");
Ok(Self { pool })
} else {
tracing::warn!(
"PostgreSQL TLS is disabled - connection is unencrypted. \
Set postgres.tls_enabled = true to enable encryption."
);
Ok(Self { pool })
let pool = pool_config
.create_pool(Some(Runtime::Tokio1), NoTls)
.map_err(|e| {
PinakesError::Database(format!("failed to create connection pool: {e}"))
})?;
// Verify connectivity
let _ = pool.get().await.map_err(|e| {
PinakesError::Database(format!("failed to connect to postgres: {e}"))
})?;
Ok(Self { pool })
}
}
}
@ -3229,6 +3284,167 @@ impl StorageBackend for PostgresBackend {
.await?;
Ok(affected)
}
// ===== Session Management =====
async fn create_session(&self, session: &crate::storage::SessionData) -> Result<()> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
client
.execute(
"INSERT INTO sessions (session_token, user_id, username, role, created_at, expires_at, last_accessed)
VALUES ($1, $2, $3, $4, $5, $6, $7)",
&[
&session.session_token,
&session.user_id,
&session.username,
&session.role,
&session.created_at,
&session.expires_at,
&session.last_accessed,
],
)
.await?;
Ok(())
}
async fn get_session(
&self,
session_token: &str,
) -> Result<Option<crate::storage::SessionData>> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
let row = client
.query_opt(
"SELECT session_token, user_id, username, role, created_at, expires_at, last_accessed
FROM sessions WHERE session_token = $1",
&[&session_token],
)
.await?;
Ok(row.map(|r| crate::storage::SessionData {
session_token: r.get(0),
user_id: r.get(1),
username: r.get(2),
role: r.get(3),
created_at: r.get(4),
expires_at: r.get(5),
last_accessed: r.get(6),
}))
}
async fn touch_session(&self, session_token: &str) -> Result<()> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
let now = chrono::Utc::now();
client
.execute(
"UPDATE sessions SET last_accessed = $1 WHERE session_token = $2",
&[&now, &session_token],
)
.await?;
Ok(())
}
async fn delete_session(&self, session_token: &str) -> Result<()> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
client
.execute(
"DELETE FROM sessions WHERE session_token = $1",
&[&session_token],
)
.await?;
Ok(())
}
async fn delete_user_sessions(&self, username: &str) -> Result<u64> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
let affected = client
.execute("DELETE FROM sessions WHERE username = $1", &[&username])
.await?;
Ok(affected)
}
async fn delete_expired_sessions(&self) -> Result<u64> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
let now = chrono::Utc::now();
let affected = client
.execute("DELETE FROM sessions WHERE expires_at < $1", &[&now])
.await?;
Ok(affected)
}
async fn list_active_sessions(
&self,
username: Option<&str>,
) -> Result<Vec<crate::storage::SessionData>> {
let client = self
.pool
.get()
.await
.map_err(|e| PinakesError::Database(format!("pool error: {e}")))?;
let now = chrono::Utc::now();
let rows = if let Some(user) = username {
client
.query(
"SELECT session_token, user_id, username, role, created_at, expires_at, last_accessed
FROM sessions WHERE expires_at > $1 AND username = $2
ORDER BY last_accessed DESC",
&[&now, &user],
)
.await?
} else {
client
.query(
"SELECT session_token, user_id, username, role, created_at, expires_at, last_accessed
FROM sessions WHERE expires_at > $1
ORDER BY last_accessed DESC",
&[&now],
)
.await?
};
Ok(rows
.into_iter()
.map(|r| crate::storage::SessionData {
session_token: r.get(0),
user_id: r.get(1),
username: r.get(2),
role: r.get(3),
created_at: r.get(4),
expires_at: r.get(5),
last_accessed: r.get(6),
})
.collect())
}
}
impl PostgresBackend {