Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
500 changes: 198 additions & 302 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ actix-web = "4.15.0"
actix-governor = "0.10"
aes-gcm = "0.11"
async-trait = "0.1"
argon2 = "0.5.3"
argon2 = "0.6.0"
base64 = { version = "0.23", default-features = false, features = ["std"] }
bloomfilter = "3.0.1"
bytes = "1"
Expand All @@ -80,19 +80,18 @@ hmac = "0.13"
hkdf = "0.13"
http = "1"
httpdate = "1"
ipnet = "2.12.1"
ipnet = "2.12.2"
lettre = { version = "0.11", default-features = false, features = ["builder", "smtp-transport", "tokio1-rustls-tls"] }
md-5 = "0.11"
moka = { version = "0.12.16", features = ["future"] }
parking_lot = "0.12"
percent-encoding = "2.3"
proc-macro2 = "1"
quick-xml = { version = "0.41", default-features = false }
quick-xml = { version = "0.42", default-features = false }
quote = "1"
openidconnect = { version = "4.0.1", default-features = false }
rand = { version = "0.10.2", default-features = false, features = ["std", "std_rng", "thread_rng"] }
rand_core_06 = { package = "rand_core", version = "0.6", features = ["getrandom"] }
redis = { version = "1.6.0", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] }
redis = { version = "1.7.0", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] }
reqwest = { version = "0.13", features = ["json"] }
sea-orm = { version = "2.0.2", features = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "macros", "runtime-tokio-rustls"] }
sea-orm-migration = { version = "2.0.2", default-features = false, features = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-rustls"] }
Expand Down
1 change: 0 additions & 1 deletion crates/aster_forge_crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ argon2.workspace = true
base64.workspace = true
hkdf.workspace = true
hmac.workspace = true
rand_core_06.workspace = true
sha2.workspace = true
thiserror.workspace = true

Expand Down
23 changes: 8 additions & 15 deletions crates/aster_forge_crypto/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ use crate::{CryptoError, Result};
use argon2::{
Algorithm, Argon2, Params, Version,
password_hash::{
Error as PasswordHashError, Output, PasswordHash, PasswordHasher, PasswordVerifier,
SaltString,
Error as PasswordHashError, PasswordHasher, PasswordVerifier,
phc::{Output, PasswordHash},
},
};
use hmac::{Hmac, KeyInit, Mac};
use rand_core_06::OsRng;
use sha2::{Digest, Sha256};
use std::fmt::Write;

Expand Down Expand Up @@ -311,9 +310,8 @@ pub fn hash_password(password: &str) -> Result<String> {
/// Returns an error when the policy's Argon2 parameters are invalid or the password-hash operation
/// fails.
pub fn hash_password_with_policy(password: &str, policy: &PasswordHashPolicy) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
password_hasher(policy.work_factor)?
.hash_password(password.as_bytes(), &salt)
.hash_password(password.as_bytes())
.map(|hash| hash.to_string())
.map_err(CryptoError::password_hash)
}
Expand Down Expand Up @@ -353,7 +351,7 @@ pub fn verify_password_with_policy(
is_valid: true,
needs_rehash,
}),
Err(PasswordHashError::Password) => Ok(PasswordHashVerification {
Err(PasswordHashError::PasswordInvalid) => Ok(PasswordHashVerification {
is_valid: false,
needs_rehash: false,
}),
Expand All @@ -370,7 +368,7 @@ fn password_hasher(work_factor: PasswordHashWorkFactor) -> Result<Argon2<'static
}

fn validate_stored_password_hash(
parsed: &PasswordHash<'_>,
parsed: &PasswordHash,
limits: PasswordHashVerificationLimits,
) -> Result<Params> {
if parsed.algorithm.as_str() != "argon2id" {
Expand Down Expand Up @@ -431,18 +429,14 @@ fn validate_stored_password_hash(
}

fn password_hash_needs_rehash(
parsed: &PasswordHash<'_>,
parsed: &PasswordHash,
params: &Params,
current: PasswordHashWorkFactor,
) -> Result<bool> {
let salt = parsed
.salt
.ok_or_else(|| CryptoError::password_hash("password hash is missing a salt"))?;
let mut salt_bytes = [0_u8; 64];
let salt_length = salt
.decode_b64(&mut salt_bytes)
.map_err(CryptoError::password_hash)?
.len();
let salt_length = salt.as_ref().len();
let output_length = params
.output_len()
.unwrap_or(DEFAULT_PASSWORD_HASH_OUTPUT_LENGTH);
Expand Down Expand Up @@ -533,7 +527,6 @@ pub fn new_sha256() -> Sha256 {
#[cfg(test)]
mod tests {
use super::*;
use argon2::password_hash::SaltString;
use sha2::Digest;

fn lightweight_policy() -> PasswordHashPolicy {
Expand All @@ -553,7 +546,7 @@ mod tests {
) -> String {
password_hasher(work_factor)
.unwrap()
.hash_password(password.as_bytes(), &SaltString::encode_b64(salt).unwrap())
.hash_password_with_salt(password.as_bytes(), salt)
.unwrap()
.to_string()
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aster_forge_test/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ impl SharedContainerState {

/// Removes resources owned by exited processes, including processes from this nextest run.
///
/// PostgreSQL uses this rolling policy because retaining every isolated database until the
/// `PostgreSQL` uses this rolling policy because retaining every isolated database until the
/// next run can exhaust ephemeral CI disks. Live processes and suite-scoped resources remain
/// registered, so concurrent tests and reusable templates are not disturbed.
pub(crate) fn prune_stale_during_current_execution(&mut self) -> Vec<String> {
Expand Down
111 changes: 75 additions & 36 deletions crates/aster_forge_webdav/src/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ use crate::protocol::DavProtocolError;
use crate::{
DavBodyError, DavBodyPolicy, DavCapabilityContext, DavCapabilityProvider,
DavCapabilitySnapshot, DavCapabilityTarget, DavConditionalPlan, DavConditionalResource,
DavFileSystem, DavIfEvaluationError, DavLockSystem, DavMethod, DavMutationCredentials, DavPath,
DavRequestHead, DavRequestOrigin, DavRequestTarget, DavResponse, DavResponseBody, IfHeader,
DavFileSystem, DavIfEvaluationError, DavLockEnforcementError, DavLockSystem, DavMethod,
DavMutationCredentials, DavParentCollectionError, DavPath, DavRequestHead, DavRequestOrigin,
DavRequestTarget, DavResponse, DavResponseBody, IfHeader,
};

/// Request body prepared according to the selected `WebDAV` method contract.
Expand Down Expand Up @@ -80,32 +81,29 @@ pub fn request_target<'a>(
DavRequestHead::parse_target(&uri, mount_path, &origin)
}

/// Resolves the product declaration and maps the validated capability snapshot to Actix.
/// Resolves and validates the product capability declaration.
///
/// # Errors
///
/// Returns an error response when provider lookup or capability validation fails.
/// Returns a typed failure when provider lookup or capability validation fails.
pub async fn capability_snapshot<Provider: DavCapabilityProvider>(
provider: &Provider,
target: &DavCapabilityTarget,
context: &DavCapabilityContext,
) -> Result<DavCapabilitySnapshot, HttpResponse> {
crate::plan_capabilities_with_provider(provider, target, context)
.await
.map_err(|error| into_response(crate::capability_evaluation_error_response(&error)))
) -> Result<DavCapabilitySnapshot, crate::DavCapabilityEvaluationError> {
crate::plan_capabilities_with_provider(provider, target, context).await
}

/// Applies the resource-aware dispatch gate to an Actix request method.
///
/// # Errors
///
/// Returns a 405 response when the snapshot does not dispatch the request method.
/// Returns [`crate::DavMethodGateError`] when the snapshot does not dispatch the request method.
pub fn gate_request_method(
request: &HttpRequest,
snapshot: &DavCapabilitySnapshot,
) -> Result<DavMethod, HttpResponse> {
) -> Result<DavMethod, crate::DavMethodGateError> {
crate::gate_method(DavMethod::from_name(request.method().as_str()), snapshot)
.map_err(|_| into_response(crate::method_not_allowed_response(snapshot)))
}

/// Converts a transport-neutral response into an Actix response.
Expand Down Expand Up @@ -151,20 +149,20 @@ pub fn protocol_error_response(error: DavProtocolError) -> HttpResponse {
into_response(crate::protocol_error_response(&error))
}

/// Copies Actix headers into the transport-neutral map and maps malformed input to a response.
/// Copies Actix headers into the transport-neutral map.
///
/// # Errors
///
/// Returns an error response when an Actix header cannot be represented by `http` 1.x.
pub fn converted_headers(source: &actix_header::HeaderMap) -> Result<HeaderMap, HttpResponse> {
convert_header_map(source).map_err(protocol_error_response)
/// Returns [`DavProtocolError`] when an Actix header cannot be represented by `http` 1.x.
pub fn converted_headers(source: &actix_header::HeaderMap) -> Result<HeaderMap, DavProtocolError> {
convert_header_map(source)
}

/// Resolves and enforces a parsed `WebDAV` `If` header through the canonical backend ports.
///
/// # Errors
///
/// Returns an error response when DAV `If` evaluation or backend access fails.
/// Returns [`DavIfEvaluationError`] when DAV `If` evaluation or backend access fails.
pub async fn enforce_if_header_with_backends(
if_header: Option<&IfHeader>,
filesystem: &dyn DavFileSystem,
Expand All @@ -173,8 +171,8 @@ pub async fn enforce_if_header_with_backends(
prefix: &str,
request_scheme: &str,
request_host: &str,
) -> Result<(), HttpResponse> {
match crate::enforce_if_header_with_backends(
) -> Result<(), DavIfEvaluationError> {
crate::enforce_if_header_with_backends(
if_header,
filesystem,
lock_system,
Expand All @@ -184,20 +182,13 @@ pub async fn enforce_if_header_with_backends(
request_host,
)
.await
{
Ok(()) => Ok(()),
Err(DavIfEvaluationError::Protocol(error)) => Err(protocol_error_response(error)),
Err(DavIfEvaluationError::Backend(error)) => {
Err(into_response(crate::backend_error_response(&error)))
}
}
}

/// Enforces resource lock submission and maps the protocol response to Actix.
/// Enforces resource lock submission and returns a compact typed failure.
///
/// # Errors
///
/// Returns an error response when a conflicting lock exists or lock lookup fails.
/// Returns [`DavLockEnforcementError`] when a conflicting lock exists or lock lookup fails.
pub async fn enforce_unlocked(
lock_system: &dyn DavLockSystem,
path: &DavPath,
Expand All @@ -206,7 +197,7 @@ pub async fn enforce_unlocked(
if_header: Option<&IfHeader>,
request_scheme: &str,
request_host: &str,
) -> Result<DavMutationCredentials, HttpResponse> {
) -> Result<DavMutationCredentials, DavLockEnforcementError> {
crate::enforce_unlocked(
lock_system,
path,
Expand All @@ -217,22 +208,21 @@ pub async fn enforce_unlocked(
request_host,
)
.await
.map_err(into_response)
}

/// Enforces lock submission for the canonical parent and maps the response to Actix.
/// Enforces lock submission for the canonical parent and returns a compact typed failure.
///
/// # Errors
///
/// Returns an error response when the parent is locked or lock lookup fails.
/// Returns [`DavLockEnforcementError`] when the parent is locked or lock lookup fails.
pub async fn enforce_parent_unlocked(
lock_system: &dyn DavLockSystem,
path: &DavPath,
prefix: &str,
if_header: Option<&IfHeader>,
request_scheme: &str,
request_host: &str,
) -> Result<DavMutationCredentials, HttpResponse> {
) -> Result<DavMutationCredentials, DavLockEnforcementError> {
crate::enforce_parent_unlocked(
lock_system,
path,
Expand All @@ -242,22 +232,71 @@ pub async fn enforce_parent_unlocked(
request_host,
)
.await
.map_err(into_response)
}

/// Converts Actix headers and runs the method-aware conditional request planner.
///
/// # Errors
///
/// Returns an error response when header conversion or conditional planning fails.
/// Returns a compact typed failure when header conversion or conditional planning fails.
pub fn plan_http_conditionals(
headers: &actix_header::HeaderMap,
method: DavMethod,
resource: DavConditionalResource<'_>,
) -> Result<DavConditionalPlan, HttpResponse> {
) -> Result<DavConditionalPlan, crate::DavConditionalPlanError> {
let headers = converted_headers(headers)?;
crate::plan_http_conditionals(method, &headers, resource)
.map_err(|error| into_response(crate::conditional_plan_error_response(&error)))
}

/// Maps capability evaluation failures to Actix at the handler boundary.
#[must_use]
pub fn capability_error_response(error: &crate::DavCapabilityEvaluationError) -> HttpResponse {
into_response(crate::capability_evaluation_error_response(error))
}

/// Maps a rejected method gate to the canonical 405 Actix response.
#[must_use]
pub fn method_gate_error_response(snapshot: &DavCapabilitySnapshot) -> HttpResponse {
into_response(crate::method_not_allowed_response(snapshot))
}

/// Maps conditional planning failures to Actix at the handler boundary.
#[must_use]
pub fn conditional_plan_error_response(error: &crate::DavConditionalPlanError) -> HttpResponse {
into_response(crate::conditional_plan_error_response(error))
}

/// Maps lock-enforcement failures to Actix at the handler boundary.
#[must_use]
pub fn lock_enforcement_error_response(
error: DavLockEnforcementError,
prefix: &str,
) -> HttpResponse {
match error {
DavLockEnforcementError::Backend(error) => {
into_response(crate::backend_error_response(&error))
}
DavLockEnforcementError::Conflict { path } => into_response(
crate::lock_conflict_response(prefix, &path)
.unwrap_or_else(|_| DavResponse::empty(http::StatusCode::INTERNAL_SERVER_ERROR)),
),
}
}

/// Maps parent-collection enforcement failures to Actix at the handler boundary.
#[must_use]
pub fn parent_collection_error_response(error: DavParentCollectionError) -> HttpResponse {
match error {
DavParentCollectionError::MethodNotAllowed => into_response(
crate::mutation_plan_error_response(crate::DavMutationPlanError::MethodNotAllowed),
),
DavParentCollectionError::Conflict => into_response(crate::mutation_plan_error_response(
crate::DavMutationPlanError::Conflict,
)),
DavParentCollectionError::Backend(error) => {
into_response(crate::backend_error_response(&error))
}
}
}

/// Copies Actix header types into the transport-neutral `http` 1.x map.
Expand Down
21 changes: 11 additions & 10 deletions crates/aster_forge_webdav/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@ pub use extension::{
DavResourceStateSet, extension_body_kind, extension_methods,
};
pub use lock::{
DavLockPlan, DavLockPlanError, enforce_parent_unlocked, enforce_unlocked,
lock_acquire_success_response, lock_conflict_response, lock_discovery_element,
lock_limit_response, lock_refresh_success_response, lock_xml_error_response, plan_lock_request,
unlock_success_response, unlock_token_mismatch_response, unsubmitted_lock_conflicts,
DavLockEnforcementError, DavLockPlan, DavLockPlanError, enforce_parent_unlocked,
enforce_unlocked, lock_acquire_success_response, lock_conflict_response,
lock_discovery_element, lock_limit_response, lock_refresh_success_response,
lock_xml_error_response, plan_lock_request, unlock_success_response,
unlock_token_mismatch_response, unsubmitted_lock_conflicts,
};
pub use multistatus::{
DavMultiStatusError, DavMultiStatusErrorKind, DavMultiStatusLimits, DavMultiStatusProgress,
Expand Down Expand Up @@ -154,12 +155,12 @@ pub use request::{
};
pub use resource::{
DavCopyMoveMethod, DavCopyMovePlan, DavMutationFailure, DavMutationPlanError,
DavMutationResponseError, collection_created_response, delete_success_response,
enforce_parent_collection, is_descendant_path, mutation_multistatus_response,
mutation_multistatus_response_with_limits, mutation_plan_error_response,
mutation_success_response, plan_copy_move_request, replace_relative_prefix,
resource_identity_path, same_resource_path, validate_collection_create_target,
validate_delete_target,
DavMutationResponseError, DavParentCollectionError, collection_created_response,
delete_success_response, enforce_parent_collection, is_descendant_path,
mutation_multistatus_response, mutation_multistatus_response_with_limits,
mutation_plan_error_response, mutation_success_response, plan_copy_move_request,
replace_relative_prefix, resource_identity_path, same_resource_path,
validate_collection_create_target, validate_delete_target,
};
pub use response::{
DavBodyError, DavDownloadBody, DavDownloadPlan, DavDownloadPlanError, DavMultiRangeLimits,
Expand Down
Loading
Loading