Skip to content

Commit db7c081

Browse files
auth: Delete telemetry credentials during logout
Ensure `sce auth logout` attempts both WorkOS and telemetry credential deletion, preserving the existing output contract while surfacing storage failures. Document the encrypted auth DB lifecycle, security boundaries, and completed implementation. Plan: telemetry-credential-storage (T03) Co-authored-by: SCE <sce@crocoder.dev>
1 parent 510ed6a commit db7c081

7 files changed

Lines changed: 153 additions & 15 deletions

File tree

‎cli/src/services/auth_command/mod.rs‎

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use crate::services::config;
1414
use crate::services::error::{CliError, UserError};
1515
use crate::services::output_format::OutputFormat;
1616
use crate::services::style::{label, prompt_label, prompt_value, success, value};
17-
use crate::services::token_storage::{self, StoredTokens};
17+
use crate::services::telemetry_credential_storage;
18+
use crate::services::token_storage::{self, StoredTokens, TokenStorageError};
1819

1920
pub const NAME: &str = "auth";
2021

@@ -84,10 +85,29 @@ pub async fn run_login(format: AuthFormat) -> Result<String, CliError> {
8485
}
8586

8687
pub async fn run_logout(format: AuthFormat) -> Result<String, CliError> {
87-
let deleted = token_storage::delete_tokens()
88-
.await
89-
.map_err(auth_storage_error)?;
90-
render_logout_result(deleted, format).map_err(unexpected_auth_command_error)
88+
run_logout_with(
89+
format,
90+
token_storage::delete_tokens(),
91+
telemetry_credential_storage::delete_telemetry_credential(),
92+
)
93+
.await
94+
}
95+
96+
async fn run_logout_with<W, T>(
97+
format: AuthFormat,
98+
delete_workos: W,
99+
delete_telemetry: T,
100+
) -> Result<String, CliError>
101+
where
102+
W: Future<Output = Result<bool, TokenStorageError>>,
103+
T: Future<Output = Result<bool, TokenStorageError>>,
104+
{
105+
let workos_result = delete_workos.await;
106+
let telemetry_result = delete_telemetry.await;
107+
let workos_deleted = workos_result.map_err(auth_storage_error)?;
108+
let telemetry_deleted = telemetry_result.map_err(auth_storage_error)?;
109+
render_logout_result(workos_deleted || telemetry_deleted, format)
110+
.map_err(unexpected_auth_command_error)
91111
}
92112

93113
pub async fn run_whoami(format: AuthFormat) -> Result<String, CliError> {
@@ -443,10 +463,57 @@ fn map_whoami_control_plane_error(error: &ControlPlaneError) -> CliError {
443463
)
444464
}
445465

446-
fn auth_storage_error(error: crate::services::token_storage::TokenStorageError) -> CliError {
466+
fn auth_storage_error(error: TokenStorageError) -> CliError {
447467
CliError::user_with_source(UserError::AuthStorageUnavailable, error)
448468
}
449469

450470
fn unexpected_auth_command_error(error: anyhow::Error) -> CliError {
451471
CliError::user_with_source(UserError::UnexpectedFailure, error)
452472
}
473+
474+
#[cfg(test)]
475+
mod tests {
476+
use std::sync::atomic::{AtomicBool, Ordering};
477+
478+
use super::*;
479+
480+
fn storage_failure() -> TokenStorageError {
481+
TokenStorageError::Database("failed".to_string())
482+
}
483+
484+
#[tokio::test]
485+
async fn telemetry_credential_logout_reports_removal_when_only_telemetry_deleted() {
486+
let output = run_logout_with(AuthFormat::Json, async { Ok(false) }, async { Ok(true) })
487+
.await
488+
.expect("logout should succeed");
489+
assert!(output.contains("\"credentials_removed\": true"));
490+
}
491+
492+
#[tokio::test]
493+
async fn telemetry_credential_logout_attempts_both_deletions_on_telemetry_failure() {
494+
let workos_called = AtomicBool::new(false);
495+
let result = run_logout_with(
496+
AuthFormat::Json,
497+
async {
498+
workos_called.store(true, Ordering::SeqCst);
499+
Ok(true)
500+
},
501+
async { Err(storage_failure()) },
502+
)
503+
.await;
504+
assert!(result.is_err());
505+
assert!(workos_called.load(Ordering::SeqCst));
506+
}
507+
508+
#[tokio::test]
509+
async fn telemetry_credential_logout_deletes_telemetry_on_workos_failure() {
510+
let telemetry_called = AtomicBool::new(false);
511+
let result = run_logout_with(AuthFormat::Json, async { Err(storage_failure()) }, async {
512+
telemetry_called.store(true, Ordering::SeqCst);
513+
Ok(true)
514+
})
515+
.await;
516+
assert!(result.is_err());
517+
assert!(telemetry_called.load(Ordering::SeqCst));
518+
}
519+
}

‎cli/src/services/telemetry_credential_storage.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ pub async fn load_telemetry_credential() -> Result<Option<TelemetryCredential>,
4444
load_with(get_auth_db().await?, current_unix_timestamp_seconds()?).await
4545
}
4646

47-
#[allow(dead_code)]
4847
pub async fn delete_telemetry_credential() -> Result<bool, TokenStorageError> {
4948
delete_with(get_auth_db().await?).await
5049
}

‎context/context-map.md‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎context/glossary.md‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎context/plans/telemetry-credential-storage.md‎

Lines changed: 44 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)