Skip to content

Commit bf78290

Browse files
only1dreamgenesamuel2926i39-art
andauthored
fix(backend-2fa): fix pre-existing compile breakage on main (#1279)
backend-2fa did not compile at all on main: - 7 TwoFactorHandlers constructors never initialized the enroll_lock field after it was added, causing E0063 struct-literal errors. - TenantScopedStore had all the right inherent methods but was never declared as `impl TwoFactorStore for TenantScopedStore`, so `Arc::new(scoped_store)` could not coerce to Arc<dyn TwoFactorStore> (E0277) in TwoFactorHandlers::for_tenant. Also fixes what was blocking `cargo test` specifically (separate from the two `cargo build` errors above): two missing imports and one moved-value error in tests.rs, plus makes the test-only `test_two_factor_store` helper accessible from tests.rs so a for-tenant test exercises the real shared store instead of an unrelated local one. Not in scope / not touched here (pre-existing, unrelated, disclosed in the PR description): 4 test failures revealed now that the suite can actually run, and ~135 clippy warnings-as-errors across the crate. Co-authored-by: samuel2926i39-art <samuel2926i39-art@users.noreply.github.com>
1 parent 372ab8f commit bf78290

3 files changed

Lines changed: 164 additions & 8 deletions

File tree

backend-2fa/src/handlers.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fn verify_token_with_replay_protection(
4040
}
4141

4242
#[cfg(test)]
43-
fn test_two_factor_store() -> Arc<InMemoryStore> {
43+
pub(crate) fn test_two_factor_store() -> Arc<InMemoryStore> {
4444
std::thread_local! {
4545
static STORE: Arc<InMemoryStore> = Arc::new(InMemoryStore::default());
4646
}
@@ -440,6 +440,7 @@ impl TwoFactorHandlers {
440440
limiter: Arc::new(InMemoryRateLimiter::default()),
441441
store: Arc::new(scoped_store),
442442
issuer: "PetChain".to_string(),
443+
enroll_lock: Arc::new(Mutex::new(())),
443444
}
444445
}
445446

@@ -448,6 +449,7 @@ impl TwoFactorHandlers {
448449
limiter: Arc::new(InMemoryRateLimiter::default()),
449450
store: two_factor_store(),
450451
issuer: default_issuer(),
452+
enroll_lock: Arc::new(Mutex::new(())),
451453
}
452454
}
453455

@@ -494,6 +496,7 @@ impl TwoFactorHandlers {
494496
limiter: lim,
495497
store: two_factor_store(),
496498
issuer: default_issuer(),
499+
enroll_lock: Arc::new(Mutex::new(())),
497500
}
498501
}
499502

@@ -507,6 +510,7 @@ impl TwoFactorHandlers {
507510
limiter,
508511
store: two_factor_store(),
509512
issuer: default_issuer(),
513+
enroll_lock: Arc::new(Mutex::new(())),
510514
}
511515
}
512516

@@ -515,6 +519,7 @@ impl TwoFactorHandlers {
515519
limiter: Arc::new(InMemoryRateLimiter::default()),
516520
store,
517521
issuer: default_issuer(),
522+
enroll_lock: Arc::new(Mutex::new(())),
518523
}
519524
}
520525

@@ -554,6 +559,7 @@ impl TwoFactorHandlers {
554559
limiter,
555560
store,
556561
issuer: default_issuer(),
562+
enroll_lock: Arc::new(Mutex::new(())),
557563
}
558564
}
559565

@@ -571,6 +577,7 @@ impl TwoFactorHandlers {
571577
limiter,
572578
store,
573579
issuer: "PetChain".to_string(),
580+
enroll_lock: Arc::new(Mutex::new(())),
574581
}
575582
}
576583

backend-2fa/src/tests.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5441,8 +5441,9 @@ mod api_error_logging_tests {
54415441
mod algorithm_upgrade_tests {
54425442
use crate::handlers::{
54435443
clear_two_factor_store_for_tests, get_two_factor_data_for_tests,
5444-
overwrite_two_factor_data_for_tests, AuthenticatedUser, EnableTwoFactorRequest,
5445-
LoginWithTwoFactorRequest, TwoFactorHandlers, UpgradeAlgorithmRequest,
5444+
overwrite_two_factor_data_for_tests, test_two_factor_store, AuthenticatedUser,
5445+
DisableTwoFactorRequest, EnableTwoFactorRequest, LoginWithTwoFactorRequest,
5446+
RecoverWithBackupRequest, TwoFactorHandlers, UpgradeAlgorithmRequest,
54465447
VerifyTwoFactorRequest,
54475448
};
54485449
use crate::two_factor::{TotpConfig, TwoFactorAuth, TwoFactorData};
@@ -6445,7 +6446,7 @@ mod algorithm_upgrade_tests {
64456446
&caller("charlie"),
64466447
VerifyTwoFactorRequest {
64476448
user_id: "charlie".to_string(),
6448-
token: token_a,
6449+
token: token_a.clone(),
64496450
},
64506451
)
64516452
.unwrap();
@@ -6545,10 +6546,8 @@ mod algorithm_upgrade_tests {
65456546
clear_two_factor_store_for_tests();
65466547
let custom_limiter: Arc<dyn RateLimiter> = Arc::new(InMemoryRateLimiter::default());
65476548
let config = crate::two_factor::TenantConfig::new("tenant-custom");
6548-
let scoped_store = crate::two_factor::TenantScopedStore::new(
6549-
test_two_factor_store(),
6550-
config,
6551-
);
6549+
let scoped_store =
6550+
crate::two_factor::TenantScopedStore::new(test_two_factor_store(), config);
65526551

65536552
let handlers = TwoFactorHandlers::with_store_and_limiter(
65546553
Arc::new(scoped_store),

backend-2fa/src/two_factor.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,156 @@ impl TenantScopedStore {
13641364
}
13651365
}
13661366

1367+
/// `TenantScopedStore` as a `TwoFactorStore` trait object, so it can be
1368+
/// stored behind `Arc<dyn TwoFactorStore>` (as `TwoFactorHandlers::store`
1369+
/// is) and used interchangeably with any other backing store.
1370+
///
1371+
/// Per-user methods delegate to the tenant-prefixed inherent methods above
1372+
/// (`self.key(user_id)`), preserving tenant isolation. The handful of
1373+
/// store-wide/admin methods (`list_users`, `admin_disable_two_fa`'s target
1374+
/// lookup being the exception — see below, `get_canary_accounts`,
1375+
/// `list_locked_users`, `get_recovery_usage_log`) have no per-user key to
1376+
/// scope by and are only ever invoked directly against the raw underlying
1377+
/// store today (see `AdminHandlers`), never through a `TenantScopedStore`
1378+
/// trait object — they delegate straight to `self.inner` unscoped, matching
1379+
/// that existing usage pattern rather than inventing new tenant-filtering
1380+
/// behavior.
1381+
impl TwoFactorStore for TenantScopedStore {
1382+
fn save(&self, user_id: &str, data: TwoFactorData) -> Result<(), String> {
1383+
self.inner.save(&self.key(user_id), data)
1384+
}
1385+
1386+
fn get(&self, user_id: &str) -> Result<TwoFactorData, String> {
1387+
self.inner.get(&self.key(user_id))
1388+
}
1389+
1390+
fn delete(&self, user_id: &str) -> Result<(), String> {
1391+
TenantScopedStore::delete(self, user_id)
1392+
}
1393+
1394+
fn update_enabled(&self, user_id: &str, enabled: bool) -> Result<(), String> {
1395+
self.inner.update_enabled(&self.key(user_id), enabled)
1396+
}
1397+
1398+
fn update_backup_codes(&self, user_id: &str, codes: Vec<String>) -> Result<(), String> {
1399+
self.inner.update_backup_codes(&self.key(user_id), codes)
1400+
}
1401+
1402+
fn log_recovery_code_usage(
1403+
&self,
1404+
user_id: &str,
1405+
code_index: i32,
1406+
ip_address: Option<&str>,
1407+
) -> Result<(), String> {
1408+
self.inner
1409+
.log_recovery_code_usage(&self.key(user_id), code_index, ip_address)
1410+
}
1411+
1412+
fn get_recovery_usage_log(
1413+
&self,
1414+
page: u32,
1415+
page_size: u32,
1416+
) -> Result<Vec<RecoveryCodeUsageLog>, String> {
1417+
self.inner.get_recovery_usage_log(page, page_size)
1418+
}
1419+
1420+
fn list_users(&self, page: u32, page_size: u32) -> Result<Vec<UserTwoFactorSummary>, String> {
1421+
self.inner.list_users(page, page_size)
1422+
}
1423+
1424+
fn admin_disable_two_fa(&self, user_id: &str, admin_id: &str) -> Result<(), String> {
1425+
self.inner
1426+
.admin_disable_two_fa(&self.key(user_id), admin_id)
1427+
}
1428+
1429+
fn get_audit_log(
1430+
&self,
1431+
user_id: &str,
1432+
page: u32,
1433+
page_size: u32,
1434+
) -> Result<Vec<AuditLogEntry>, String> {
1435+
self.inner
1436+
.get_audit_log(&self.key(user_id), page, page_size)
1437+
}
1438+
1439+
fn append_audit_log(
1440+
&self,
1441+
user_id: &str,
1442+
event: &str,
1443+
actor: &str,
1444+
metadata: Option<&str>,
1445+
) -> Result<(), String> {
1446+
self.inner
1447+
.append_audit_log(&self.key(user_id), event, actor, metadata)
1448+
}
1449+
1450+
fn set_canary(&self, user_id: &str, is_canary: bool) -> Result<(), String> {
1451+
self.inner.set_canary(&self.key(user_id), is_canary)
1452+
}
1453+
1454+
fn is_canary(&self, user_id: &str) -> bool {
1455+
self.inner.is_canary(&self.key(user_id))
1456+
}
1457+
1458+
fn get_canary_accounts(&self) -> Result<Vec<String>, String> {
1459+
self.inner.get_canary_accounts()
1460+
}
1461+
1462+
fn get_lockout_state(&self, user_id: &str) -> Result<TwoFactorLockoutState, String> {
1463+
self.inner.get_lockout_state(&self.key(user_id))
1464+
}
1465+
1466+
fn record_failed_two_fa_attempt(
1467+
&self,
1468+
user_id: &str,
1469+
_lockout_threshold: u32,
1470+
) -> Result<TwoFactorLockoutState, String> {
1471+
// Tenant-scoped stores always enforce their own configured
1472+
// lockout_threshold (self.config.lockout_threshold), not a
1473+
// caller-supplied one, matching the inherent
1474+
// `TenantScopedStore::record_failed_two_fa_attempt` above.
1475+
self.inner
1476+
.record_failed_two_fa_attempt(&self.key(user_id), self.config.lockout_threshold)
1477+
}
1478+
1479+
fn reset_two_fa_failures(&self, user_id: &str) -> Result<(), String> {
1480+
self.inner.reset_two_fa_failures(&self.key(user_id))
1481+
}
1482+
1483+
fn set_last_used_step(&self, user_id: &str, step: u64) -> Result<(), String> {
1484+
self.inner.set_last_used_step(&self.key(user_id), step)
1485+
}
1486+
1487+
fn unlock_two_fa_account(&self, user_id: &str, actor: &str) -> Result<(), String> {
1488+
self.inner.unlock_two_fa_account(&self.key(user_id), actor)
1489+
}
1490+
1491+
fn list_locked_users(&self) -> Result<Vec<LockedUserSummary>, String> {
1492+
self.inner.list_locked_users()
1493+
}
1494+
1495+
fn reset_recovery_log(&self, user_id: &str) -> Result<(), String> {
1496+
self.inner.reset_recovery_log(&self.key(user_id))
1497+
}
1498+
1499+
fn revoke_session(&self, user_id: &str, session_id: &str) -> Result<(), String> {
1500+
self.inner.revoke_session(&self.key(user_id), session_id)
1501+
}
1502+
1503+
fn revoke_all_sessions(&self, user_id: &str) -> Result<(), String> {
1504+
self.inner.revoke_all_sessions(&self.key(user_id))
1505+
}
1506+
1507+
fn is_session_revoked(&self, user_id: &str, session_id: &str, issued_at: u64) -> bool {
1508+
self.inner
1509+
.is_session_revoked(&self.key(user_id), session_id, issued_at)
1510+
}
1511+
1512+
fn check_retry_after(&self, user_id: &str) -> Result<(), String> {
1513+
self.inner.check_retry_after(&self.key(user_id))
1514+
}
1515+
}
1516+
13671517
/// Registry of tenants. Super-admin provisions tenants; all lookups are
13681518
/// scoped so cross-tenant access is structurally impossible.
13691519
#[derive(Default, Clone)]

0 commit comments

Comments
 (0)