From c28b7088b6fad045060a52b6e1a2249e876090e3 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 11 Jun 2025 20:20:04 -0600 Subject: refactor: extract domain model --- src/domain/conversions.rs | 40 +++++++++++++++++++------ src/domain/dto.rs | 2 +- src/domain/mappers.rs | 40 +++++++++++++++++++------ src/domain/mod.rs | 2 +- src/domain/models.rs | 8 ++--- src/domain/queries.rs | 19 ++++-------- src/domain/repositories.rs | 9 ++++-- src/domain/services.rs | 70 +++++++++++++++++++++++++++++++++++++------- src/domain/specifications.rs | 40 +++++++++++++++---------- src/domain/unit_of_work.rs | 10 +++---- 10 files changed, 169 insertions(+), 71 deletions(-) (limited to 'src/domain') diff --git a/src/domain/conversions.rs b/src/domain/conversions.rs index 53e6062..13a1b9b 100644 --- a/src/domain/conversions.rs +++ b/src/domain/conversions.rs @@ -1,4 +1,4 @@ -use crate::database::{DbAccessToken, DbAuthCode, DbAuditLog, DbOAuthClient}; +use crate::database::{DbAccessToken, DbAuditLog, DbAuthCode, DbOAuthClient}; use crate::domain::models::*; use anyhow::Result; @@ -18,9 +18,21 @@ pub trait ToDb { impl FromDb for OAuthClient { fn from_db(db_client: DbOAuthClient) -> Result { let redirect_uris: Vec = serde_json::from_str(&db_client.redirect_uris)?; - let scopes: Vec = db_client.scopes.split_whitespace().map(|s| s.to_string()).collect(); - let grant_types: Vec = db_client.grant_types.split_whitespace().map(|s| s.to_string()).collect(); - let response_types: Vec = db_client.response_types.split_whitespace().map(|s| s.to_string()).collect(); + let scopes: Vec = db_client + .scopes + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + let grant_types: Vec = db_client + .grant_types + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + let response_types: Vec = db_client + .response_types + .split_whitespace() + .map(|s| s.to_string()) + .collect(); Ok(OAuthClient { client_id: db_client.client_id, @@ -57,8 +69,13 @@ impl ToDb for OAuthClient { // Authorization Code conversions impl FromDb for AuthorizationCode { fn from_db(db_code: DbAuthCode) -> Result { - let scopes = db_code.scope - .map(|s| s.split_whitespace().map(|scope| scope.to_string()).collect()) + let scopes = db_code + .scope + .map(|s| { + s.split_whitespace() + .map(|scope| scope.to_string()) + .collect() + }) .unwrap_or_default(); Ok(AuthorizationCode { @@ -103,8 +120,13 @@ impl ToDb for AuthorizationCode { // Access Token conversions impl FromDb for AccessToken { fn from_db(db_token: DbAccessToken) -> Result { - let scopes = db_token.scope - .map(|s| s.split_whitespace().map(|scope| scope.to_string()).collect()) + let scopes = db_token + .scope + .map(|s| { + s.split_whitespace() + .map(|scope| scope.to_string()) + .collect() + }) .unwrap_or_default(); Ok(AccessToken { @@ -171,4 +193,4 @@ impl ToDb for AuditEvent { success: self.success, }) } -} \ No newline at end of file +} diff --git a/src/domain/dto.rs b/src/domain/dto.rs index 336db61..1c342bc 100644 --- a/src/domain/dto.rs +++ b/src/domain/dto.rs @@ -131,4 +131,4 @@ impl From for ErrorResponseDto { error_uri: error.uri, } } -} \ No newline at end of file +} diff --git a/src/domain/mappers.rs b/src/domain/mappers.rs index 6efe276..405b08b 100644 --- a/src/domain/mappers.rs +++ b/src/domain/mappers.rs @@ -1,4 +1,4 @@ -use crate::database::{DbAccessToken, DbAuthCode, DbAuditLog, DbOAuthClient}; +use crate::database::{DbAccessToken, DbAuditLog, DbAuthCode, DbOAuthClient}; use crate::domain::models::*; use anyhow::Result; @@ -14,9 +14,21 @@ pub struct OAuthClientMapper; impl DataMapper for OAuthClientMapper { fn to_domain(&self, db_client: DbOAuthClient) -> Result { let redirect_uris: Vec = serde_json::from_str(&db_client.redirect_uris)?; - let scopes: Vec = db_client.scopes.split_whitespace().map(|s| s.to_string()).collect(); - let grant_types: Vec = db_client.grant_types.split_whitespace().map(|s| s.to_string()).collect(); - let response_types: Vec = db_client.response_types.split_whitespace().map(|s| s.to_string()).collect(); + let scopes: Vec = db_client + .scopes + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + let grant_types: Vec = db_client + .grant_types + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + let response_types: Vec = db_client + .response_types + .split_whitespace() + .map(|s| s.to_string()) + .collect(); Ok(OAuthClient { client_id: db_client.client_id, @@ -53,8 +65,13 @@ pub struct AuthCodeMapper; impl DataMapper for AuthCodeMapper { fn to_domain(&self, db_code: DbAuthCode) -> Result { - let scopes = db_code.scope - .map(|s| s.split_whitespace().map(|scope| scope.to_string()).collect()) + let scopes = db_code + .scope + .map(|s| { + s.split_whitespace() + .map(|scope| scope.to_string()) + .collect() + }) .unwrap_or_default(); Ok(AuthorizationCode { @@ -99,8 +116,13 @@ pub struct AccessTokenMapper; impl DataMapper for AccessTokenMapper { fn to_domain(&self, db_token: DbAccessToken) -> Result { - let scopes = db_token.scope - .map(|s| s.split_whitespace().map(|scope| scope.to_string()).collect()) + let scopes = db_token + .scope + .map(|s| { + s.split_whitespace() + .map(|scope| scope.to_string()) + .collect() + }) .unwrap_or_default(); Ok(AccessToken { @@ -206,4 +228,4 @@ impl Default for MapperRegistry { fn default() -> Self { Self::new() } -} \ No newline at end of file +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 9a8bfca..7ba3b00 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -16,4 +16,4 @@ pub use queries::*; pub use repositories::*; pub use services::*; pub use specifications::*; -pub use unit_of_work::*; \ No newline at end of file +pub use unit_of_work::*; diff --git a/src/domain/models.rs b/src/domain/models.rs index 85b554f..26e6df3 100644 --- a/src/domain/models.rs +++ b/src/domain/models.rs @@ -108,9 +108,9 @@ pub struct AuthorizationRequest { #[derive(Debug, Clone, PartialEq)] pub struct TokenRequest { pub grant_type: String, - pub code: Option, // For authorization_code grant - pub refresh_token: Option, // For refresh_token grant - pub redirect_uri: Option, // For authorization_code grant + pub code: Option, // For authorization_code grant + pub refresh_token: Option, // For refresh_token grant + pub redirect_uri: Option, // For authorization_code grant pub client_id: String, pub client_secret: Option, // PKCE @@ -220,4 +220,4 @@ impl Scope { description: Some("Access to user email address".to_string()), } } -} \ No newline at end of file +} diff --git a/src/domain/queries.rs b/src/domain/queries.rs index d4eb19e..88fb480 100644 --- a/src/domain/queries.rs +++ b/src/domain/queries.rs @@ -277,31 +277,24 @@ pub struct CommonQueries; impl CommonQueries { /// Get recent failed login attempts (security monitoring) pub fn recent_failed_logins() -> FailedAuthQuery { - FailedAuthQuery::new() - .last_24_hours() - .min_attempts(3) + FailedAuthQuery::new().last_24_hours().min_attempts(3) } /// Get audit trail for a specific client pub fn client_audit_trail(client_id: &str) -> AuditEventsQuery { - AuditEventsQuery::new() - .for_client(client_id) - .limit(1000) + AuditEventsQuery::new().for_client(client_id).limit(1000) } /// Get token usage statistics for the last 30 days pub fn monthly_token_usage() -> TokenUsageQuery { let now = Utc::now(); let thirty_days_ago = now - chrono::Duration::days(30); - - TokenUsageQuery::new(TokenUsageGroupBy::Day) - .date_range(thirty_days_ago, now) + + TokenUsageQuery::new(TokenUsageGroupBy::Day).date_range(thirty_days_ago, now) } /// Get all active clients with OpenID scope pub fn openid_clients() -> OAuthClientsQuery { - OAuthClientsQuery::new() - .active_only() - .with_scope("openid") + OAuthClientsQuery::new().active_only().with_scope("openid") } -} \ No newline at end of file +} diff --git a/src/domain/repositories.rs b/src/domain/repositories.rs index 1aa4f33..3622373 100644 --- a/src/domain/repositories.rs +++ b/src/domain/repositories.rs @@ -36,6 +36,11 @@ pub trait DomainAuditRepository: Send + Sync { /// Domain-focused repository for rate limiting pub trait DomainRateRepository: Send + Sync { fn check_rate_limit(&self, limit: &RateLimit) -> Result; // Returns current count - fn increment_rate_limit(&self, identifier: &str, endpoint: &str, window_minutes: u32) -> Result; + fn increment_rate_limit( + &self, + identifier: &str, + endpoint: &str, + window_minutes: u32, + ) -> Result; fn cleanup_old_rate_limits(&self) -> Result<()>; -} \ No newline at end of file +} diff --git a/src/domain/services.rs b/src/domain/services.rs index 2c23cdc..0e22ddb 100644 --- a/src/domain/services.rs +++ b/src/domain/services.rs @@ -3,10 +3,22 @@ use anyhow::Result; /// Domain service for OAuth2 authorization flow pub trait AuthorizationService: Send + Sync { - fn authorize(&self, request: &AuthorizationRequest, user: &User) -> Result; + fn authorize( + &self, + request: &AuthorizationRequest, + user: &User, + ) -> Result; fn validate_client(&self, client_id: &str) -> Result; - fn validate_redirect_uri(&self, client: &OAuthClient, redirect_uri: &str) -> Result<(), OAuthError>; - fn validate_scopes(&self, client: &OAuthClient, requested_scopes: &[String]) -> Result, OAuthError>; + fn validate_redirect_uri( + &self, + client: &OAuthClient, + redirect_uri: &str, + ) -> Result<(), OAuthError>; + fn validate_scopes( + &self, + client: &OAuthClient, + requested_scopes: &[String], + ) -> Result, OAuthError>; } /// Domain service for OAuth2 token operations @@ -23,28 +35,59 @@ pub trait ClientService: Send + Sync { fn get_client(&self, client_id: &str) -> Result>; fn update_client(&self, client: &OAuthClient) -> Result<()>; fn delete_client(&self, client_id: &str) -> Result<()>; - fn authenticate_client(&self, client_id: &str, client_secret: &str) -> Result; + fn authenticate_client( + &self, + client_id: &str, + client_secret: &str, + ) -> Result; } /// Domain service for user management pub trait UserService: Send + Sync { fn get_user(&self, user_id: &str) -> Result>; fn authenticate_user(&self, username: &str, password: &str) -> Result; - fn is_user_authorized(&self, user: &User, client: &OAuthClient, scopes: &[String]) -> Result; + fn is_user_authorized( + &self, + user: &User, + client: &OAuthClient, + scopes: &[String], + ) -> Result; } /// Domain service for audit logging pub trait AuditService: Send + Sync { - fn log_authorization_attempt(&self, request: &AuthorizationRequest, user: Option<&User>, success: bool, ip_address: Option<&str>) -> Result<()>; - fn log_token_request(&self, request: &TokenRequest, success: bool, ip_address: Option<&str>) -> Result<()>; - fn log_token_introspection(&self, token_hash: &str, client_id: &str, success: bool) -> Result<()>; + fn log_authorization_attempt( + &self, + request: &AuthorizationRequest, + user: Option<&User>, + success: bool, + ip_address: Option<&str>, + ) -> Result<()>; + fn log_token_request( + &self, + request: &TokenRequest, + success: bool, + ip_address: Option<&str>, + ) -> Result<()>; + fn log_token_introspection( + &self, + token_hash: &str, + client_id: &str, + success: bool, + ) -> Result<()>; fn log_token_revocation(&self, token_hash: &str, client_id: &str, success: bool) -> Result<()>; } /// Domain service for rate limiting pub trait RateLimitService: Send + Sync { fn check_rate_limit(&self, identifier: &str, endpoint: &str) -> Result<(), OAuthError>; - fn is_rate_limited(&self, identifier: &str, endpoint: &str, max_requests: u32, window_minutes: u32) -> Result; + fn is_rate_limited( + &self, + identifier: &str, + endpoint: &str, + max_requests: u32, + window_minutes: u32, + ) -> Result; } /// Domain service for PKCE operations @@ -57,7 +100,12 @@ pub trait PkceService: Send + Sync { /// Domain service for JWT operations pub trait JwtService: Send + Sync { fn generate_access_token(&self, claims: &TokenClaims) -> Result; - fn generate_refresh_token(&self, client_id: &str, user_id: &str, scopes: &[String]) -> Result; + fn generate_refresh_token( + &self, + client_id: &str, + user_id: &str, + scopes: &[String], + ) -> Result; fn validate_token(&self, token: &str) -> Result; fn get_jwks(&self) -> Result; // JSON Web Key Set -} \ No newline at end of file +} diff --git a/src/domain/specifications.rs b/src/domain/specifications.rs index 3237d1b..76aafcc 100644 --- a/src/domain/specifications.rs +++ b/src/domain/specifications.rs @@ -22,7 +22,7 @@ impl Specification for AndSpecification { fn is_satisfied_by(&self, candidate: &T) -> bool { self.left.is_satisfied_by(candidate) && self.right.is_satisfied_by(candidate) } - + fn reason_for_failure(&self, candidate: &T) -> Option { if !self.left.is_satisfied_by(candidate) { self.left.reason_for_failure(candidate) @@ -40,7 +40,7 @@ impl Specification for ActiveClientSpecification { fn is_satisfied_by(&self, client: &OAuthClient) -> bool { client.is_active } - + fn reason_for_failure(&self, _client: &OAuthClient) -> Option { Some("Client is not active".to_string()) } @@ -60,7 +60,7 @@ impl Specification for ValidRedirectUriSpecification { fn is_satisfied_by(&self, client: &OAuthClient) -> bool { client.redirect_uris.contains(&self.redirect_uri) } - + fn reason_for_failure(&self, _client: &OAuthClient) -> Option { Some(format!("Invalid redirect_uri: {}", self.redirect_uri)) } @@ -78,15 +78,19 @@ impl SupportedScopesSpecification { impl Specification for SupportedScopesSpecification { fn is_satisfied_by(&self, client: &OAuthClient) -> bool { - self.requested_scopes.iter().all(|scope| client.scopes.contains(scope)) + self.requested_scopes + .iter() + .all(|scope| client.scopes.contains(scope)) } - + fn reason_for_failure(&self, client: &OAuthClient) -> Option { - let unsupported: Vec<_> = self.requested_scopes.iter() + let unsupported: Vec<_> = self + .requested_scopes + .iter() .filter(|scope| !client.scopes.contains(scope)) .cloned() .collect(); - + if !unsupported.is_empty() { Some(format!("Unsupported scopes: {}", unsupported.join(", "))) } else { @@ -101,7 +105,7 @@ impl Specification for UnusedAuthCodeSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { !code.is_used } - + fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("Authorization code has already been used".to_string()) } @@ -112,7 +116,7 @@ impl Specification for ValidAuthCodeSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { chrono::Utc::now() < code.expires_at } - + fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("Authorization code has expired".to_string()) } @@ -132,7 +136,7 @@ impl Specification for MatchingClientSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { code.client_id == self.client_id } - + fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("Client ID mismatch".to_string()) } @@ -153,14 +157,18 @@ impl Specification for ValidPkceSpecification { fn is_satisfied_by(&self, code: &AuthorizationCode) -> bool { if let Some(challenge) = &code.code_challenge { let method = code.code_challenge_method.as_deref().unwrap_or("plain"); - crate::oauth::pkce::verify_code_challenge(&self.code_verifier, challenge, - &crate::oauth::pkce::CodeChallengeMethod::from_str(method).unwrap_or(crate::oauth::pkce::CodeChallengeMethod::Plain) - ).is_ok() + crate::oauth::pkce::verify_code_challenge( + &self.code_verifier, + challenge, + &crate::oauth::pkce::CodeChallengeMethod::from_str(method) + .unwrap_or(crate::oauth::pkce::CodeChallengeMethod::Plain), + ) + .is_ok() } else { true // No PKCE required } } - + fn reason_for_failure(&self, _code: &AuthorizationCode) -> Option { Some("PKCE verification failed".to_string()) } @@ -172,7 +180,7 @@ impl Specification for ValidTokenSpecification { fn is_satisfied_by(&self, token: &AccessToken) -> bool { !token.is_revoked && chrono::Utc::now() < token.expires_at } - + fn reason_for_failure(&self, token: &AccessToken) -> Option { if token.is_revoked { Some("Token has been revoked".to_string()) @@ -191,4 +199,4 @@ pub trait SpecificationExt: Specification + Sized + 'static { } } -impl + 'static> SpecificationExt for S {} \ No newline at end of file +impl + 'static> SpecificationExt for S {} diff --git a/src/domain/unit_of_work.rs b/src/domain/unit_of_work.rs index db8294a..7d6a0e3 100644 --- a/src/domain/unit_of_work.rs +++ b/src/domain/unit_of_work.rs @@ -11,10 +11,10 @@ pub trait UnitOfWork: Send + Sync { pub trait Transaction: Send + Sync { /// Commit all changes in this transaction fn commit(self: Box) -> Result<()>; - + /// Rollback all changes in this transaction fn rollback(self: Box) -> Result<()>; - + /// Get repositories within this transaction context fn client_repository(&self) -> Arc; fn auth_code_repository(&self) -> Arc; @@ -31,7 +31,7 @@ impl OAuthUnitOfWork { pub fn new(uow: Arc) -> Self { Self { uow } } - + /// Execute OAuth2 authorization code exchange atomically pub fn exchange_authorization_code(&self, operation: F) -> Result<()> where @@ -46,7 +46,7 @@ impl OAuthUnitOfWork { } } } - + /// Execute token refresh atomically pub fn refresh_tokens(&self, operation: F) -> Result<()> where @@ -61,4 +61,4 @@ impl OAuthUnitOfWork { } } } -} \ No newline at end of file +} -- cgit v1.2.3