Skip to content

Commit 2aa8629

Browse files
committed
refactor: replace raw String fields with SecretString and NatsToken across source configs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 061ec51 commit 2aa8629

14 files changed

Lines changed: 294 additions & 195 deletions

File tree

rsworkspace/crates/trogon-gateway/src/config.rs

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::time::Duration;
44

55
use confique::Config;
66
use trogon_nats::{NatsAuth, NatsToken};
7+
use trogon_std::SecretString;
78

89
#[derive(Debug)]
910
pub enum ConfigError {
@@ -272,15 +273,39 @@ fn resolve_nats(section: &NatsConfig) -> trogon_nats::NatsConfig {
272273
fn resolve_github(
273274
section: GithubConfig,
274275
nats: &trogon_nats::NatsConfig,
275-
_errors: &mut Vec<String>,
276+
errors: &mut Vec<String>,
276277
) -> Option<trogon_source_github::GithubConfig> {
277-
let webhook_secret = section.webhook_secret.filter(|s| !s.is_empty())?;
278+
let secret_str = section.webhook_secret.filter(|s| !s.is_empty())?;
279+
280+
let webhook_secret = match SecretString::new(secret_str) {
281+
Ok(s) => s,
282+
Err(e) => {
283+
errors.push(format!("github: invalid webhook_secret: {e}"));
284+
return None;
285+
}
286+
};
287+
288+
let subject_prefix = match NatsToken::new(section.subject_prefix) {
289+
Ok(t) => t,
290+
Err(e) => {
291+
errors.push(format!("github: invalid subject_prefix: {e:?}"));
292+
return None;
293+
}
294+
};
295+
296+
let stream_name = match NatsToken::new(section.stream_name) {
297+
Ok(t) => t,
298+
Err(e) => {
299+
errors.push(format!("github: invalid stream_name: {e:?}"));
300+
return None;
301+
}
302+
};
278303

279304
Some(trogon_source_github::GithubConfig {
280305
webhook_secret,
281306
port: 0,
282-
subject_prefix: section.subject_prefix,
283-
stream_name: section.stream_name,
307+
subject_prefix,
308+
stream_name,
284309
stream_max_age: Duration::from_secs(section.stream_max_age_secs),
285310
nats_ack_timeout: Duration::from_secs(section.nats_ack_timeout_secs),
286311
nats: nats.clone(),
@@ -296,11 +321,19 @@ fn resolve_discord(
296321

297322
let mode = match mode_str.to_ascii_lowercase().as_str() {
298323
"gateway" => {
299-
let Some(bot_token) = section.bot_token.filter(|s| !s.is_empty()) else {
324+
let Some(token_str) = section.bot_token.filter(|s| !s.is_empty()) else {
300325
errors.push("discord: bot_token is required when mode=gateway".to_string());
301326
return None;
302327
};
303328

329+
let bot_token = match SecretString::new(token_str) {
330+
Ok(s) => s,
331+
Err(e) => {
332+
errors.push(format!("discord: invalid bot_token: {e}"));
333+
return None;
334+
}
335+
};
336+
304337
let intents = if let Some(ref s) = section.gateway_intents {
305338
match trogon_source_discord::config::parse_intents(s) {
306339
Ok(i) => i,
@@ -373,7 +406,15 @@ fn resolve_slack(
373406
nats: &trogon_nats::NatsConfig,
374407
errors: &mut Vec<String>,
375408
) -> Option<trogon_source_slack::SlackConfig> {
376-
let signing_secret = section.signing_secret.filter(|s| !s.is_empty())?;
409+
let secret_str = section.signing_secret.filter(|s| !s.is_empty())?;
410+
411+
let signing_secret = match SecretString::new(secret_str) {
412+
Ok(s) => s,
413+
Err(e) => {
414+
errors.push(format!("slack: invalid signing_secret: {e}"));
415+
return None;
416+
}
417+
};
377418

378419
let subject_prefix = match NatsToken::new(section.subject_prefix) {
379420
Ok(t) => t,
@@ -406,15 +447,39 @@ fn resolve_slack(
406447
fn resolve_telegram(
407448
section: TelegramConfig,
408449
nats: &trogon_nats::NatsConfig,
409-
_errors: &mut Vec<String>,
450+
errors: &mut Vec<String>,
410451
) -> Option<trogon_source_telegram::TelegramSourceConfig> {
411-
let webhook_secret = section.webhook_secret.filter(|s| !s.is_empty())?;
452+
let secret_str = section.webhook_secret.filter(|s| !s.is_empty())?;
453+
454+
let webhook_secret = match SecretString::new(secret_str) {
455+
Ok(s) => s,
456+
Err(e) => {
457+
errors.push(format!("telegram: invalid webhook_secret: {e}"));
458+
return None;
459+
}
460+
};
461+
462+
let subject_prefix = match NatsToken::new(section.subject_prefix) {
463+
Ok(t) => t,
464+
Err(e) => {
465+
errors.push(format!("telegram: invalid subject_prefix: {e:?}"));
466+
return None;
467+
}
468+
};
469+
470+
let stream_name = match NatsToken::new(section.stream_name) {
471+
Ok(t) => t,
472+
Err(e) => {
473+
errors.push(format!("telegram: invalid stream_name: {e:?}"));
474+
return None;
475+
}
476+
};
412477

413478
Some(trogon_source_telegram::TelegramSourceConfig {
414479
webhook_secret,
415480
port: 0,
416-
subject_prefix: section.subject_prefix,
417-
stream_name: section.stream_name,
481+
subject_prefix,
482+
stream_name,
418483
stream_max_age: Duration::from_secs(section.stream_max_age_secs),
419484
nats_ack_timeout: Duration::from_secs(section.nats_ack_timeout_secs),
420485
nats: nats.clone(),
@@ -469,7 +534,15 @@ fn resolve_linear(
469534
nats: &trogon_nats::NatsConfig,
470535
errors: &mut Vec<String>,
471536
) -> Option<trogon_source_linear::LinearConfig> {
472-
let webhook_secret = section.webhook_secret.filter(|s| !s.is_empty())?;
537+
let secret_str = section.webhook_secret.filter(|s| !s.is_empty())?;
538+
539+
let webhook_secret = match SecretString::new(secret_str) {
540+
Ok(s) => s,
541+
Err(e) => {
542+
errors.push(format!("linear: invalid webhook_secret: {e}"));
543+
return None;
544+
}
545+
};
473546

474547
let subject_prefix = match NatsToken::new(section.subject_prefix) {
475548
Ok(t) => t,

rsworkspace/crates/trogon-gateway/src/serve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ fn mount_sources(
185185
} => {
186186
let token = bot_token.clone();
187187
join_set.spawn(async move {
188-
trogon_source_discord::gateway_runner::run(p, &cfg, &token, intents).await;
188+
trogon_source_discord::gateway_runner::run(p, &cfg, token.as_str(), intents).await;
189189
("discord-gateway", Ok(()))
190190
});
191191
info!(source = "discord", "gateway mode spawned");

rsworkspace/crates/trogon-source-discord/src/config.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::time::Duration;
22

33
use ed25519_dalek::VerifyingKey;
44
use trogon_nats::{NatsConfig, NatsToken};
5+
use trogon_std::SecretString;
56
use trogon_std::env::ReadEnv;
67
use twilight_model::gateway::Intents;
78

@@ -14,7 +15,7 @@ use crate::constants::{
1415
use crate::signature;
1516

1617
pub enum SourceMode {
17-
Gateway { bot_token: String, intents: Intents },
18+
Gateway { bot_token: SecretString, intents: Intents },
1819
Webhook { public_key: VerifyingKey },
1920
}
2021

@@ -37,11 +38,13 @@ impl DiscordConfig {
3738

3839
let mode = match mode_str.to_ascii_lowercase().as_str() {
3940
"gateway" => {
40-
let bot_token = env
41-
.var("DISCORD_BOT_TOKEN")
42-
.ok()
43-
.filter(|s| !s.is_empty())
44-
.expect("DISCORD_BOT_TOKEN is required when DISCORD_MODE=gateway");
41+
let bot_token = SecretString::new(
42+
env.var("DISCORD_BOT_TOKEN")
43+
.ok()
44+
.filter(|s| !s.is_empty())
45+
.expect("DISCORD_BOT_TOKEN is required when DISCORD_MODE=gateway"),
46+
)
47+
.expect("DISCORD_BOT_TOKEN must not be empty");
4548

4649
let intents = env
4750
.var("DISCORD_GATEWAY_INTENTS")
@@ -214,7 +217,7 @@ mod tests {
214217

215218
match &config.mode {
216219
SourceMode::Gateway { bot_token, intents } => {
217-
assert_eq!(bot_token, "test-token");
220+
assert_eq!(bot_token.as_str(), "test-token");
218221
assert_eq!(*intents, default_intents());
219222
assert!(intents.contains(Intents::GUILDS));
220223
assert!(intents.contains(Intents::GUILD_MESSAGES));

rsworkspace/crates/trogon-source-github/src/config.rs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::time::Duration;
22

3-
use trogon_nats::NatsConfig;
3+
use trogon_nats::{NatsConfig, NatsToken};
4+
use trogon_std::SecretString;
45
use trogon_std::env::ReadEnv;
56

67
use crate::constants::{
@@ -19,10 +20,10 @@ use crate::constants::{
1920
/// - `GITHUB_NATS_ACK_TIMEOUT_SECS`: NATS ack timeout in seconds (default: 10)
2021
/// - Standard `NATS_*` variables for NATS connection (see `trogon-nats`)
2122
pub struct GithubConfig {
22-
pub webhook_secret: String,
23+
pub webhook_secret: SecretString,
2324
pub port: u16,
24-
pub subject_prefix: String,
25-
pub stream_name: String,
25+
pub subject_prefix: NatsToken,
26+
pub stream_name: NatsToken,
2627
pub stream_max_age: Duration,
2728
pub nats_ack_timeout: Duration,
2829
pub nats: NatsConfig,
@@ -31,22 +32,28 @@ pub struct GithubConfig {
3132
impl GithubConfig {
3233
pub fn from_env<E: ReadEnv>(env: &E) -> Self {
3334
Self {
34-
webhook_secret: env
35-
.var("GITHUB_WEBHOOK_SECRET")
36-
.ok()
37-
.filter(|s| !s.is_empty())
38-
.expect("GITHUB_WEBHOOK_SECRET is required"),
35+
webhook_secret: SecretString::new(
36+
env.var("GITHUB_WEBHOOK_SECRET")
37+
.ok()
38+
.filter(|s| !s.is_empty())
39+
.expect("GITHUB_WEBHOOK_SECRET is required"),
40+
)
41+
.expect("GITHUB_WEBHOOK_SECRET must not be empty"),
3942
port: env
4043
.var("GITHUB_WEBHOOK_PORT")
4144
.ok()
4245
.and_then(|p| p.parse().ok())
4346
.unwrap_or(DEFAULT_PORT),
44-
subject_prefix: env
45-
.var("GITHUB_SUBJECT_PREFIX")
46-
.unwrap_or_else(|_| DEFAULT_SUBJECT_PREFIX.to_string()),
47-
stream_name: env
48-
.var("GITHUB_STREAM_NAME")
49-
.unwrap_or_else(|_| DEFAULT_STREAM_NAME.to_string()),
47+
subject_prefix: NatsToken::new(
48+
env.var("GITHUB_SUBJECT_PREFIX")
49+
.unwrap_or_else(|_| DEFAULT_SUBJECT_PREFIX.to_string()),
50+
)
51+
.expect("GITHUB_SUBJECT_PREFIX is not a valid NATS token"),
52+
stream_name: NatsToken::new(
53+
env.var("GITHUB_STREAM_NAME")
54+
.unwrap_or_else(|_| DEFAULT_STREAM_NAME.to_string()),
55+
)
56+
.expect("GITHUB_STREAM_NAME is not a valid NATS token"),
5057
stream_max_age: env
5158
.var("GITHUB_STREAM_MAX_AGE_SECS")
5259
.ok()
@@ -80,10 +87,10 @@ mod tests {
8087
let env = env_with_secret();
8188
let config = GithubConfig::from_env(&env);
8289

83-
assert_eq!(config.webhook_secret, "test-secret");
90+
assert_eq!(config.webhook_secret.as_str(), "test-secret");
8491
assert_eq!(config.port, 8080);
85-
assert_eq!(config.subject_prefix, "github");
86-
assert_eq!(config.stream_name, "GITHUB");
92+
assert_eq!(config.subject_prefix.as_str(), "github");
93+
assert_eq!(config.stream_name.as_str(), "GITHUB");
8794
assert_eq!(config.stream_max_age, Duration::from_secs(7 * 24 * 60 * 60));
8895
assert_eq!(config.nats_ack_timeout, Duration::from_secs(10));
8996
}
@@ -100,10 +107,10 @@ mod tests {
100107

101108
let config = GithubConfig::from_env(&env);
102109

103-
assert_eq!(config.webhook_secret, "my-secret");
110+
assert_eq!(config.webhook_secret.as_str(), "my-secret");
104111
assert_eq!(config.port, 9090);
105-
assert_eq!(config.subject_prefix, "gh");
106-
assert_eq!(config.stream_name, "GH_EVENTS");
112+
assert_eq!(config.subject_prefix.as_str(), "gh");
113+
assert_eq!(config.stream_name.as_str(), "GH_EVENTS");
107114
assert_eq!(config.stream_max_age, Duration::from_secs(3600));
108115
assert_eq!(config.nats_ack_timeout, Duration::from_secs(30));
109116
}

rsworkspace/crates/trogon-source-github/src/server.rs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use crate::constants::{
77
NATS_HEADER_EVENT,
88
};
99
use crate::signature;
10+
use trogon_nats::NatsToken;
11+
use trogon_std::SecretString;
1012
#[cfg(not(coverage))]
1113
use async_nats::jetstream::context::CreateStreamError;
1214
use axum::{
@@ -68,25 +70,23 @@ fn outcome_to_status<E: fmt::Display>(outcome: PublishOutcome<E>) -> StatusCode
6870
#[derive(Clone)]
6971
struct AppState<P: JetStreamPublisher, S: ObjectStorePut> {
7072
publisher: ClaimCheckPublisher<P, S>,
71-
webhook_secret: String,
72-
subject_prefix: String,
73+
webhook_secret: SecretString,
74+
subject_prefix: NatsToken,
7375
nats_ack_timeout: Duration,
7476
}
7577

7678
pub async fn provision<C: JetStreamContext>(js: &C, config: &GithubConfig) -> Result<(), C::Error> {
7779
js.get_or_create_stream(async_nats::jetstream::stream::Config {
78-
name: config.stream_name.clone(),
80+
name: config.stream_name.as_str().to_owned(),
7981
subjects: vec![format!("{}.>", config.subject_prefix)],
8082
max_age: config.stream_max_age,
8183
..Default::default()
8284
})
8385
.await?;
8486

87+
let stream = config.stream_name.as_str();
8588
let max_age_secs = config.stream_max_age.as_secs();
86-
info!(
87-
stream = config.stream_name,
88-
max_age_secs, "JetStream stream ready"
89-
);
89+
info!(stream, max_age_secs, "JetStream stream ready");
9090
Ok(())
9191
}
9292

@@ -170,7 +170,7 @@ async fn handle_webhook_inner<P: JetStreamPublisher, S: ObjectStorePut>(
170170

171171
match sig {
172172
Some(sig) => {
173-
if let Err(e) = signature::verify(&state.webhook_secret, &body, sig) {
173+
if let Err(e) = signature::verify(state.webhook_secret.as_str(), &body, sig) {
174174
warn!(reason = %e, "GitHub webhook signature validation failed");
175175
return StatusCode::UNAUTHORIZED;
176176
}
@@ -253,10 +253,10 @@ mod tests {
253253

254254
fn test_config() -> GithubConfig {
255255
GithubConfig {
256-
webhook_secret: TEST_SECRET.to_string(),
256+
webhook_secret: SecretString::new(TEST_SECRET).unwrap(),
257257
port: 0,
258-
subject_prefix: "github".to_string(),
259-
stream_name: "GITHUB".to_string(),
258+
subject_prefix: NatsToken::new("github").unwrap(),
259+
stream_name: NatsToken::new("GITHUB").unwrap(),
260260
stream_max_age: Duration::from_secs(3600),
261261
nats_ack_timeout: Duration::from_secs(10),
262262
nats: trogon_nats::NatsConfig::from_env(&trogon_std::env::InMemoryEnv::new()),
@@ -470,8 +470,8 @@ mod tests {
470470

471471
let state = AppState {
472472
publisher: wrap_publisher(publisher.clone()),
473-
webhook_secret: TEST_SECRET.to_string(),
474-
subject_prefix: "custom".to_string(),
473+
webhook_secret: SecretString::new(TEST_SECRET).unwrap(),
474+
subject_prefix: NatsToken::new("custom").unwrap(),
475475
nats_ack_timeout: Duration::from_secs(10),
476476
};
477477

@@ -641,8 +641,8 @@ mod tests {
641641
"test-bucket".to_string(),
642642
MaxPayload::from_server_limit(usize::MAX),
643643
),
644-
webhook_secret: TEST_SECRET.to_string(),
645-
subject_prefix: "github".to_string(),
644+
webhook_secret: SecretString::new(TEST_SECRET).unwrap(),
645+
subject_prefix: NatsToken::new("github").unwrap(),
646646
nats_ack_timeout: Duration::from_secs(10),
647647
};
648648

@@ -676,8 +676,8 @@ mod tests {
676676
"test-bucket".to_string(),
677677
MaxPayload::from_server_limit(usize::MAX),
678678
),
679-
webhook_secret: TEST_SECRET.to_string(),
680-
subject_prefix: "github".to_string(),
679+
webhook_secret: SecretString::new(TEST_SECRET).unwrap(),
680+
subject_prefix: NatsToken::new("github").unwrap(),
681681
nats_ack_timeout: Duration::from_millis(10),
682682
};
683683

0 commit comments

Comments
 (0)