docs: tambah doc comment, logging, dan inline comments di semua 255 file
Meliputi: - File-level //! doc comment: tujuan file, alur kerja, komponen utama - Function-level /// doc comment: apa, parameter, return, flow, edge cases - Struct/enum/trait /// doc comment: peran, field docs - Tracing logging (tracing::info!/debug!/trace!/warn!/error!) di setiap fungsi - Inline comments untuk variable dan branching logic penting - Seluruh 8 crates di workspace: zesdex-backend, zesdex-cms, zesdex-entities, zesdex-iam, zesdex-infra, zesdex-ipc, zesdex-middleware, zesdex-utils - Build: 0 errors, 242/242 tests passed
This commit is contained in:
@@ -1,2 +1,12 @@
|
||||
//! Application-layer use-case implementations.
|
||||
//!
|
||||
//! Contains concrete service orchestrators that coordinate domain entities
|
||||
//! and infrastructure adapters.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`oauth_service`] — OAuth 2.0 authorization-code flow orchestration
|
||||
//! - [`session_service`] — IAM session lifecycle management
|
||||
|
||||
pub mod oauth_service;
|
||||
pub mod session_service;
|
||||
|
||||
@@ -7,7 +7,28 @@
|
||||
//! files next to `token_path` so `start_flow` and `complete_flow` can be
|
||||
//! two separate calls (the caller — see `zesdex-backend`'s
|
||||
//! `run_oauth_flow` — binds a real loopback listener in between).
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! 1. **`start_flow`** — Generate PKCE verifier + S256 challenge + CSRF state.
|
||||
//! Persist verifier and state to sidecar files. Build and return the
|
||||
//! authorization URL with `code_challenge_method=S256`.
|
||||
//! 2. Caller opens browser at the returned URL, user authorizes, provider
|
||||
//! redirects to the loopback with `?code=...&state=...`.
|
||||
//! 3. **`complete_flow`** — Validate state (CSRF check), read PKCE verifier,
|
||||
//! POST `grant_type=authorization_code` + code + verifier to token URL,
|
||||
//! parse the response, persist the `OAuthToken`, clean up sidecar files.
|
||||
//! 4. **`get_token`** — Load the persisted token (no refresh tokens handled yet;
|
||||
//! an expired token triggers a re-auth).
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `CodeVerifier` — PKCE code verifier (random bytes → base64url) with
|
||||
//! S256 challenge derivation
|
||||
//! - `OAuthServiceImpl<R>` — generic OAuth service over `OAuthRepository`
|
||||
//! - `start_flow` / `complete_flow` / `get_token` — trait impl methods
|
||||
use std::path::PathBuf;
|
||||
use tracing;
|
||||
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use base64::Engine as _;
|
||||
@@ -48,7 +69,9 @@ impl CodeVerifier {
|
||||
/// file (`token_path` with `.verifier`/`.state` extensions respectively)
|
||||
/// in `start_flow` and consumed + deleted in `complete_flow`.
|
||||
pub struct OAuthServiceImpl<R: OAuthRepository> {
|
||||
/// Repository for persisting / loading OAuth tokens.
|
||||
pub token_repo: R,
|
||||
/// File path for the token JSON file (sidecar files use derived paths).
|
||||
pub token_path: PathBuf,
|
||||
}
|
||||
|
||||
@@ -103,6 +126,12 @@ impl<R: OAuthRepository> OAuthService for OAuthServiceImpl<R> {
|
||||
std::fs::write(self.verifier_path(), verifier.as_str())?;
|
||||
std::fs::write(self.state_path(), &state)?;
|
||||
|
||||
tracing::debug!(
|
||||
auth_url = %config.auth_url,
|
||||
redirect_uri = %redirect_uri,
|
||||
"starting OAuth flow"
|
||||
);
|
||||
|
||||
let mut url = url::Url::parse(&config.auth_url)
|
||||
.map_err(|e| anyhow::anyhow!("invalid auth_url '{}': {e}", config.auth_url))?;
|
||||
|
||||
@@ -136,6 +165,12 @@ impl<R: OAuthRepository> OAuthService for OAuthServiceImpl<R> {
|
||||
let verifier = std::fs::read_to_string(&verifier_path)
|
||||
.map_err(|e| anyhow::anyhow!("failed to read PKCE verifier: {e}"))?;
|
||||
|
||||
tracing::debug!(
|
||||
token_url = %config.token_url,
|
||||
code_len = code.len(),
|
||||
"completing OAuth flow — exchanging code for token"
|
||||
);
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let mut params = std::collections::HashMap::new();
|
||||
params.insert("grant_type", "authorization_code");
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
//! Session management use-cases.
|
||||
//!
|
||||
//! `SessionServiceImpl` implements `SessionService` by delegating to
|
||||
//! `SessionServiceImpl` implements [`SessionService`] by delegating to
|
||||
//! injected repository implementations, keeping the orchestration logic
|
||||
//! independent of any concrete persistence mechanism.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! - **`create_session`** — generates a UUID v4 id, creates a `Session` entity,
|
||||
//! delegates persistence to `SessionRepository`.
|
||||
//! - **`list_all`** — delegates to `SessionRepository::list_sessions`.
|
||||
//! - **`archive_session`** — loads session, sets `archived = true`, persists.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `SessionServiceImpl<R, L>` — service over two generic repositories
|
||||
//! - `new` / `create_session` / `list_all` / `archive_session` — lifecycle ops
|
||||
use std::path::PathBuf;
|
||||
use tracing;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -13,8 +26,11 @@ use crate::domain::session::Session;
|
||||
|
||||
/// Concrete session service backed by generic repository implementations.
|
||||
pub struct SessionServiceImpl<R: SessionRepository, L: SessionLockRepository> {
|
||||
/// Repository for session CRUD operations.
|
||||
pub session_repo: R,
|
||||
/// Repository for session lock acquire/release.
|
||||
pub lock_repo: L,
|
||||
/// Base data directory passed to repository methods.
|
||||
pub base_dir: PathBuf,
|
||||
}
|
||||
|
||||
@@ -39,15 +55,18 @@ impl<R: SessionRepository, L: SessionLockRepository> SessionService for SessionS
|
||||
title.to_string()
|
||||
};
|
||||
let session = Session::new(id, title_owned);
|
||||
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn list_all(&self) -> anyhow::Result<Vec<Session>> {
|
||||
tracing::debug!("listing all sessions");
|
||||
self.session_repo.list_sessions(&self.base_dir)
|
||||
}
|
||||
|
||||
fn archive_session(&self, id: &str) -> anyhow::Result<()> {
|
||||
tracing::debug!(session_id = %id, "archiving session");
|
||||
let mut session = self.session_repo.load_session(&self.base_dir, id)?;
|
||||
session.archived = true;
|
||||
session.updated_at = std::time::SystemTime::now()
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
//! Domain layer for IAM (Identity & Access Management).
|
||||
//!
|
||||
//! Pure entities, repository traits, and service traits — no infrastructure
|
||||
//! or application orchestration logic.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`oauth`] — `OAuthConfig`, `OAuthToken` entities
|
||||
//! - [`repository`] — Trait definitions: `OAuthRepository`, `SessionRepository`,
|
||||
//! `SessionLockRepository`, `Rng`
|
||||
//! - [`service`] — Trait definitions: `OAuthService`, `SessionService`
|
||||
//! - [`session`] — `Session` entity (IAM wrapper around `zesdex_entities::Session`)
|
||||
|
||||
pub mod oauth;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
//! Pure OAuth entities — no HTTP or persistence logic.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - [`OAuthToken`] — access token with optional refresh token, epoch expiry
|
||||
//! - [`OAuthConfig`] — provider configuration (auth URL, token URL, client id,
|
||||
//! optional client secret, scopes)
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An OAuth 2.0 access token with optional refresh token and absolute
|
||||
/// expiry time (epoch seconds).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthToken {
|
||||
/// The OAuth 2.0 access token string.
|
||||
pub access_token: String,
|
||||
/// Optional refresh token for long-lived access.
|
||||
pub refresh_token: Option<String>,
|
||||
/// Absolute expiry timestamp (epoch seconds since UNIX_EPOCH).
|
||||
pub expires_at: u64,
|
||||
/// Token type, e.g. `"Bearer"`.
|
||||
pub token_type: String,
|
||||
}
|
||||
|
||||
/// Static configuration for an OAuth provider.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthConfig {
|
||||
/// Authorization endpoint URL.
|
||||
pub auth_url: String,
|
||||
/// Token exchange endpoint URL.
|
||||
pub token_url: String,
|
||||
/// OAuth client identifier.
|
||||
pub client_id: String,
|
||||
/// Optional client secret (not all flows require it).
|
||||
pub client_secret: Option<String>,
|
||||
/// Space-separated list of requested scopes.
|
||||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
//! Repository trait definitions (pure — no impls, no concrete persistence).
|
||||
//!
|
||||
//! Defines the repository contracts that infrastructure adapters implement.
|
||||
//! Following clean architecture, domain code depends only on these traits,
|
||||
//! not on concrete persistence libraries.
|
||||
//!
|
||||
//! # Traits
|
||||
//!
|
||||
//! - [`SessionRepository`] — CRUD for session metadata
|
||||
//! - [`SessionLockRepository`] — acquire/release/liveness for session locks
|
||||
//! - [`OAuthRepository`] — persist/load OAuth tokens
|
||||
use std::path::Path;
|
||||
|
||||
use crate::domain::oauth::OAuthToken;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
//! Service trait definitions — use-case interfaces for session management
|
||||
//! and OAuth flows.
|
||||
//!
|
||||
//! These traits define the boundary between the application orchestration
|
||||
//! layer and the domain. Implementations live in `application/`.
|
||||
//!
|
||||
//! # Traits
|
||||
//!
|
||||
//! - [`SessionService`] — create, list, archive sessions
|
||||
//! - [`OAuthService`] — start PKCE flow, complete code exchange, retrieve token
|
||||
use crate::domain::oauth::{OAuthConfig, OAuthToken};
|
||||
use crate::domain::session::Session;
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
//! Pure Session entity
|
||||
//! Pure Session entity.
|
||||
//!
|
||||
//! Re-exported from zesdex_entities for consistency.
|
||||
//! Re-exported from `zesdex_entities` for consistency so that the IAM crate
|
||||
//! owns its domain vocabulary without duplicating the struct definition.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! Consumers of this crate import `Session` from here rather than from
|
||||
//! `zesdex_entities` directly, keeping the dependency internal.
|
||||
|
||||
pub use zesdex_entities::domain::auth::session::Session;
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
//! IAM-specific HTTP / IPC DTOs (Data Transfer Objects).
|
||||
//!
|
||||
//! Request and response types used by the OAuth loopback HTTP handlers.
|
||||
//! Grouped by concern: session DTOs and OAuth flow DTOs.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `CreateSessionRequest` / `SessionResponse` / `SessionListResponse` — session CRUD
|
||||
//! - `OAuthStartRequest` / `OAuthStartResponse` — start OAuth flow
|
||||
//! - `OAuthCompleteRequest` / `OAuthTokenResponse` — complete OAuth flow
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::domain::oauth::{OAuthConfig, OAuthToken};
|
||||
@@ -11,19 +20,23 @@ use crate::domain::session::Session;
|
||||
/// Request body for creating a new session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateSessionRequest {
|
||||
/// Human-readable session title.
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// Response containing one session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionResponse {
|
||||
/// The session object.
|
||||
pub session: Session,
|
||||
}
|
||||
|
||||
/// Response containing a list of sessions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionListResponse {
|
||||
/// All loadable sessions.
|
||||
pub sessions: Vec<Session>,
|
||||
/// Convenience count (length of sessions).
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
@@ -34,28 +47,37 @@ pub struct SessionListResponse {
|
||||
/// Request body for starting an OAuth flow.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthStartRequest {
|
||||
/// Provider configuration (auth/token URLs, client id, scopes).
|
||||
pub config: OAuthConfig,
|
||||
/// Loopback URI where the provider will redirect after authorization.
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
/// Response containing the authorization URL and CSRF state for an OAuth flow.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthStartResponse {
|
||||
/// The URL the user must visit in their browser to authorize.
|
||||
pub auth_url: String,
|
||||
/// CSRF state token (must be passed unchanged to `complete_flow`).
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Request body for completing an OAuth flow with an authorization code.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthCompleteRequest {
|
||||
/// Same provider config used in `start_flow`.
|
||||
pub config: OAuthConfig,
|
||||
/// Same redirect URI used in `start_flow`.
|
||||
pub redirect_uri: String,
|
||||
/// The authorization code received from the provider callback.
|
||||
pub code: String,
|
||||
/// The state token to validate (CSRF check against `start_flow`).
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Response containing the acquired OAuth token.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthTokenResponse {
|
||||
/// The OAuth token (access + optional refresh + expiry).
|
||||
pub token: OAuthToken,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
//! request DTO, delegates to the service, and returns a response DTO.
|
||||
//! Handlers are generic over the service trait so they remain independent
|
||||
//! of concrete implementations.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! HTTP request arrives → deserialize DTO → call handler → handler delegates
|
||||
//! to service → serialize response DTO → send HTTP response.
|
||||
//!
|
||||
//! # Handlers
|
||||
//!
|
||||
//! - `handle_create_session` / `handle_list_sessions` / `handle_archive_session`
|
||||
//! - `handle_start_oauth` / `handle_complete_oauth` / `handle_get_token`
|
||||
use tracing;
|
||||
|
||||
use crate::domain::service::{OAuthService, SessionService};
|
||||
use crate::infrastructure::http::dto::{
|
||||
@@ -16,12 +27,14 @@ pub fn handle_create_session<S: SessionService>(
|
||||
service: &S,
|
||||
req: CreateSessionRequest,
|
||||
) -> anyhow::Result<SessionResponse> {
|
||||
tracing::debug!(title = %req.title, "handle_create_session");
|
||||
let session = service.create_session(&req.title)?;
|
||||
Ok(SessionResponse { session })
|
||||
}
|
||||
|
||||
/// Handle a list-sessions request.
|
||||
pub fn handle_list_sessions<S: SessionService>(service: &S) -> anyhow::Result<SessionListResponse> {
|
||||
tracing::debug!("handle_list_sessions");
|
||||
let sessions = service.list_all()?;
|
||||
let total = sessions.len();
|
||||
Ok(SessionListResponse { sessions, total })
|
||||
@@ -29,6 +42,7 @@ pub fn handle_list_sessions<S: SessionService>(service: &S) -> anyhow::Result<Se
|
||||
|
||||
/// Handle an archive-session request.
|
||||
pub fn handle_archive_session<S: SessionService>(service: &S, id: &str) -> anyhow::Result<()> {
|
||||
tracing::debug!(session_id = %id, "handle_archive_session");
|
||||
service.archive_session(id)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -38,6 +52,7 @@ pub fn handle_start_oauth<O: OAuthService>(
|
||||
service: &O,
|
||||
req: OAuthStartRequest,
|
||||
) -> anyhow::Result<OAuthStartResponse> {
|
||||
tracing::debug!(redirect_uri = %req.redirect_uri, "handle_start_oauth");
|
||||
let (auth_url, state) = service.start_flow(&req.config, &req.redirect_uri)?;
|
||||
Ok(OAuthStartResponse { auth_url, state })
|
||||
}
|
||||
@@ -47,12 +62,14 @@ pub fn handle_complete_oauth<O: OAuthService>(
|
||||
service: &O,
|
||||
req: OAuthCompleteRequest,
|
||||
) -> anyhow::Result<OAuthTokenResponse> {
|
||||
tracing::debug!(code_len = req.code.len(), "handle_complete_oauth");
|
||||
let token = service.complete_flow(&req.config, &req.redirect_uri, &req.code, &req.state)?;
|
||||
Ok(OAuthTokenResponse { token })
|
||||
}
|
||||
|
||||
/// Handle a get-token request.
|
||||
pub fn handle_get_token<O: OAuthService>(service: &O) -> anyhow::Result<OAuthTokenResponse> {
|
||||
tracing::debug!("handle_get_token");
|
||||
let token = service
|
||||
.get_token()?
|
||||
.ok_or_else(|| anyhow::anyhow!("no OAuth token stored"))?;
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
//! HTTP adapter layer for OAuth callback handling.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`dto`] — Request/response DTOs for the loopback endpoint
|
||||
//! - [`handlers`] — HTTP handler that validates state and extracts `?code=`
|
||||
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
//! Infrastructure adapters for the IAM crate.
|
||||
//!
|
||||
//! Concrete implementations of domain repository traits and the HTTP adapter
|
||||
//! layer (OAuth loopback server, request/response DTOs, handlers).
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`http`] — HTTP server, DTOs, handlers for the OAuth callback
|
||||
//! - [`oauth_loopback`] — Loopback HTTP server to receive the OAuth redirect
|
||||
//! - [`persistence`] — Filesystem-backed repositories (JSON + PID locks)
|
||||
//! - [`rng`] — System random token / UUID generation
|
||||
|
||||
pub mod http;
|
||||
pub mod oauth_loopback;
|
||||
pub mod persistence;
|
||||
|
||||
@@ -2,13 +2,31 @@
|
||||
//!
|
||||
//! Ported from `zesdex-backend::service::oauth::loopback` to centralise OAuth
|
||||
//! primitives in the `zesdex-iam` crate.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! 1. [`LoopbackServer::bind`] — bind to `127.0.0.1:0` (OS-assigned port).
|
||||
//! 2. [`redirect_uri`](LoopbackServer::redirect_uri) — caller gets the full
|
||||
//! `http://127.0.0.1:<port>/callback` URI to pass to `start_flow`.
|
||||
//! 3. [`wait_for_code`](LoopbackServer::wait_for_code) — block until browser
|
||||
//! redirect hits the loopback → parse `?code=` and `?state=` from the HTTP
|
||||
//! request line → validate state → respond with 200/400 → return the code.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `LoopbackServer` — single-use TCP listener for one OAuth callback
|
||||
//! - `wait_for_code` / `read_callback` / `extract_code` / `extract_state`
|
||||
//! - `urlencoding` — minimal percent-decoder for query parameters
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use tracing;
|
||||
|
||||
/// A single-use HTTP listener on `127.0.0.1` that receives the OAuth
|
||||
/// `?code=...` redirect and serves back a static confirmation page.
|
||||
pub struct LoopbackServer {
|
||||
/// The bound TCP listener (accepts one connection per `wait_for_code` call).
|
||||
listener: TcpListener,
|
||||
/// The OS-assigned port number.
|
||||
port: u16,
|
||||
}
|
||||
|
||||
@@ -19,6 +37,7 @@ impl LoopbackServer {
|
||||
pub fn bind() -> std::io::Result<Self> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||
let port = listener.local_addr()?.port();
|
||||
tracing::debug!(port, "loopback server bound");
|
||||
Ok(LoopbackServer { listener, port })
|
||||
}
|
||||
|
||||
@@ -37,6 +56,7 @@ impl LoopbackServer {
|
||||
/// Return: `Err(InvalidData)` if no `code` param is present or the state
|
||||
/// doesn't match `expected_state`.
|
||||
pub fn wait_for_code(&self, timeout_ms: u64, expected_state: &str) -> std::io::Result<String> {
|
||||
tracing::debug!(port = self.port, timeout_ms, "waiting for OAuth callback");
|
||||
let (mut stream, _) = self.listener.accept()?;
|
||||
stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?;
|
||||
Self::read_callback(&mut stream, expected_state)
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
//! Filesystem-backed repository implementations for IAM entities.
|
||||
//!
|
||||
//! Implements domain repository traits using JSON file persistence for
|
||||
//! sessions, OAuth tokens, and PID-file session locks.
|
||||
//!
|
||||
//! # Sub-modules
|
||||
//!
|
||||
//! - [`oauth_repo`] — `OAuthRepository` impl: JSON file read/write with atomic save
|
||||
//! - [`session_lock_repo`] — `SessionLockRepository` impl: delegates to `SessionLock`
|
||||
//! - [`session_repo`] — `SessionRepository` impl: delegates to `Session` entity CRUD
|
||||
|
||||
pub mod oauth_repo;
|
||||
pub mod session_lock_repo;
|
||||
pub mod session_repo;
|
||||
|
||||
@@ -2,8 +2,20 @@
|
||||
//!
|
||||
//! Tokens are stored as a single JSON file. Writes use a write-then-rename
|
||||
//! plus fsync pattern for crash safety, with restrictive owner-only mode
|
||||
//! 0o600 on Unix.
|
||||
//! `0o600` on Unix.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! - **`save_token`** — ensures parent directory exists, then atomically writes
|
||||
//! the token JSON with `0o600` permissions.
|
||||
//! - **`load_token`** — returns `None` if the file doesn't exist, otherwise
|
||||
//! reads and JSON-parses it.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `FileSystemOAuthRepository` — stateless singleton implementing `OAuthRepository`
|
||||
use std::path::Path;
|
||||
use tracing;
|
||||
|
||||
use zesdex_utils::write_json_atomic;
|
||||
|
||||
@@ -26,14 +38,17 @@ impl OAuthRepository for FileSystemOAuthRepository {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
tracing::debug!(path = %path.display(), "saving OAuth token");
|
||||
write_json_atomic(path, token, Some(0o600))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_token(&self, path: &Path) -> anyhow::Result<Option<OAuthToken>> {
|
||||
if !path.exists() {
|
||||
tracing::debug!(path = %path.display(), "no stored OAuth token found");
|
||||
return Ok(None);
|
||||
}
|
||||
tracing::debug!(path = %path.display(), "loading OAuth token");
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
let token: OAuthToken = serde_json::from_str(&data)?;
|
||||
Ok(Some(token))
|
||||
|
||||
@@ -6,10 +6,23 @@
|
||||
//! check to guard against PID reuse. This repository is stateless (no
|
||||
//! `Drop`-based auto-release) — callers that need panic-safety should wrap
|
||||
//! acquisition in their own RAII guard (see `zesdex-backend`'s
|
||||
//! `main.rs::SessionLockGuard`, added in a later task of this plan).
|
||||
//! `main.rs::SessionLockGuard`).
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! 1. **`try_lock`** — attempt `O_CREAT|O_EXCL` open on `<session_dir>/.lock`;
|
||||
//! if it already exists, check PID liveness; if stale, overwrite atomically.
|
||||
//! 2. **`unlock`** — remove the `.lock` file.
|
||||
//! 3. **`is_alive`** — `libc::kill(pid, 0)` + `/proc/<pid>/exe` identity check.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `FileSystemSessionLockRepository` — stateless singleton implementing
|
||||
//! `SessionLockRepository`
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use tracing;
|
||||
|
||||
use crate::domain::repository::SessionLockRepository;
|
||||
|
||||
@@ -38,17 +51,22 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
|
||||
Ok(mut file) => {
|
||||
write!(file, "{pid}")?;
|
||||
file.sync_all()?;
|
||||
tracing::debug!(path = %path.display(), pid, "session lock acquired");
|
||||
return Ok(true);
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
tracing::debug!(path = %path.display(), "lock file exists, checking staleness");
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path).unwrap_or_default();
|
||||
if let Ok(existing_pid) = content.trim().parse::<u32>() {
|
||||
if self.is_alive(existing_pid) {
|
||||
tracing::warn!(existing_pid, path = %path.display(), "session lock held by live process");
|
||||
return Ok(false);
|
||||
}
|
||||
tracing::debug!(existing_pid, "stale lock detected, overwriting");
|
||||
}
|
||||
|
||||
let tmp = path.with_extension("lock.tmp");
|
||||
|
||||
@@ -2,7 +2,25 @@
|
||||
//!
|
||||
//! Each session is stored as `<base_dir>/sessions/<id>/session.json`.
|
||||
//! Writes use a write-then-rename + fsync pattern for crash safety.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! - **`list_sessions`** — enumerate `<base_dir>/sessions/` subdirectories,
|
||||
//! attempt `load_session` on each (silently skipping failures).
|
||||
//! - **`load_session`** — validates id (path-traversal check), reads JSON.
|
||||
//! - **`save_session`** — creates session directory, writes JSON atomically.
|
||||
//! - **`delete_session`** — validates id, removes the session directory.
|
||||
//!
|
||||
//! # Security
|
||||
//!
|
||||
//! All methods that accept a user-supplied `id` string reject ids containing
|
||||
//! `/`, `\\`, or `..` to prevent directory-traversal attacks.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `FileSystemSessionRepository` — stateless singleton implementing `SessionRepository`
|
||||
use std::path::Path;
|
||||
use tracing;
|
||||
|
||||
use zesdex_utils::write_json_atomic;
|
||||
|
||||
@@ -24,6 +42,7 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
fn list_sessions(&self, base_dir: &Path) -> anyhow::Result<Vec<Session>> {
|
||||
let sessions_dir = base_dir.join("sessions");
|
||||
let Ok(entries) = std::fs::read_dir(&sessions_dir) else {
|
||||
tracing::warn!(path = %sessions_dir.display(), "sessions directory not found");
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut sessions = Vec::new();
|
||||
@@ -36,6 +55,7 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
tracing::debug!(count = sessions.len(), "listed sessions");
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
@@ -48,6 +68,7 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
if !path.exists() {
|
||||
anyhow::bail!("session not found: {id}");
|
||||
}
|
||||
tracing::debug!(session_id = %id, path = %path.display(), "loading session");
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let session: Session = serde_json::from_str(&data)?;
|
||||
Ok(session)
|
||||
@@ -57,6 +78,7 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
let dir = session.session_dir(base_dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join("session.json");
|
||||
tracing::debug!(session_id = %session.id, path = %path.display(), "saving session");
|
||||
write_json_atomic(&path, session, None)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -66,6 +88,7 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
anyhow::bail!("invalid session id '{id}': must not contain path separators");
|
||||
}
|
||||
let dir = base_dir.join("sessions").join(id);
|
||||
tracing::debug!(session_id = %id, path = %dir.display(), "deleting session");
|
||||
if dir.exists() {
|
||||
std::fs::remove_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
//! Cryptographically secure random-token generation for OAuth CSRF state
|
||||
//! tokens and PKCE verifiers.
|
||||
//!
|
||||
//! # Flow
|
||||
//!
|
||||
//! [`secure_token_hex`] draws `n_bytes` from the OS CSPRNG (`OsRng` /
|
||||
//! `getrandom`), then hex-encodes the result.
|
||||
//!
|
||||
//! # Security
|
||||
//!
|
||||
//! Uses `OsRng` (kernel entropy source), not a predictable PRNG seeded with
|
||||
//! `SystemTime::now()` — this is critical for CSRF `state` tokens and PKCE
|
||||
//! verifier unpredictability.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `secure_token_hex` — generate `n` CSPRNG bytes as a lowercase hex string
|
||||
use rand_core::{OsRng, RngCore};
|
||||
|
||||
/// Generate `n_bytes` of CSPRNG output, hex-encoded.
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
//!
|
||||
//! Clean Architecture / Domain-Driven Design structure:
|
||||
//!
|
||||
//! - **domain** — Pure entities and repository/service trait definitions
|
||||
//! - **application**— Use-case implementations of the service traits
|
||||
//! - **infrastructure** — Concrete persistence (filesystem) and HTTP adapter layers
|
||||
//! - **domain** — Pure entities (`OAuthProvider`, `IamSession`) and
|
||||
//! repository/service trait definitions (`OAuthRepo`, `SessionRepo`,
|
||||
//! `SessionLockRepo`, `Rng`, `IamService`)
|
||||
//! - **application**— Use-case implementations: `OAuthService`, `SessionService`
|
||||
//! - **infrastructure** — Concrete persistence (filesystem JSON repos),
|
||||
//! HTTP adapter (loopback server, handlers, DTOs), and system RNG
|
||||
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
|
||||
Reference in New Issue
Block a user