Skip to content

Commit 3b95b25

Browse files
committed
Add regression test to verify secrets are never persisted as plaintext (#2943)
1 parent a3328c8 commit 3b95b25

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

core/integration/tests/data_integrity/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818

1919
mod verify_after_server_restart;
2020
mod verify_consumer_group_partition_assignment;
21+
mod verify_no_plaintext_credentials_on_disk;
2122
mod verify_user_login_after_restart;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/* Licensed to the Apache Software Foundation (ASF) under one
2+
* or more contributor license agreements. See the NOTICE file
3+
* distributed with this work for additional information
4+
* regarding copyright ownership. The ASF licenses this file
5+
* to you under the Apache License, Version 2.0 (the
6+
* "License"); you may not use this file except in compliance
7+
* with the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
use iggy::prelude::*;
20+
use integration::iggy_harness;
21+
use std::fs;
22+
use std::path::{Path, PathBuf};
23+
24+
const USERNAME: &str = "plaintext-regression-user";
25+
const PLAINTEXT_PASSWORD: &str = "plaintext-password-regression-2943";
26+
const PAT_NAME: &str = "plaintext-regression-pat";
27+
28+
#[iggy_harness(test_client_transport = [Tcp, Http, Quic, WebSocket])]
29+
async fn should_not_persist_plaintext_password_or_pat_to_disk(harness: &mut TestHarness) {
30+
let root_client = harness.root_client().await.unwrap();
31+
root_client
32+
.create_user(USERNAME, PLAINTEXT_PASSWORD, UserStatus::Active, None)
33+
.await
34+
.unwrap();
35+
36+
let raw_pat = root_client
37+
.create_personal_access_token(PAT_NAME, IggyExpiry::NeverExpire)
38+
.await
39+
.unwrap();
40+
41+
assert!(!raw_pat.token.is_empty(), "Expected non-empty PAT value");
42+
43+
let data_path = harness.server().data_path();
44+
45+
drop(root_client);
46+
harness.stop().await.unwrap();
47+
48+
assert_secret_not_persisted(&data_path, PLAINTEXT_PASSWORD, "plaintext password");
49+
assert_secret_not_persisted(&data_path, &raw_pat.token, "raw PAT");
50+
}
51+
52+
fn assert_secret_not_persisted(root: &Path, secret: &str, secret_name: &str) {
53+
let secret = secret.as_bytes();
54+
for path in collect_files(root) {
55+
let contents = fs::read(&path).unwrap_or_else(|e| {
56+
panic!("Failed to read persisted file {}: {e}", path.display());
57+
});
58+
assert!(
59+
!contains_subslice(&contents, secret),
60+
"Found {secret_name} persisted in file {}",
61+
path.display()
62+
);
63+
}
64+
}
65+
66+
fn collect_files(root: &Path) -> Vec<PathBuf> {
67+
let mut files = Vec::new();
68+
collect_files_recursive(root, &mut files);
69+
files
70+
}
71+
72+
fn collect_files_recursive(path: &Path, files: &mut Vec<PathBuf>) {
73+
let entries = fs::read_dir(path).unwrap_or_else(|e| {
74+
panic!("Failed to read persisted directory {}: {e}", path.display());
75+
});
76+
77+
for entry in entries {
78+
let entry = entry.unwrap_or_else(|e| {
79+
panic!(
80+
"Failed to read entry in persisted directory {}: {e}",
81+
path.display()
82+
);
83+
});
84+
let entry_path = entry.path();
85+
let file_type = entry.file_type().unwrap_or_else(|e| {
86+
panic!("Failed to get file type for {}: {e}", entry_path.display());
87+
});
88+
89+
if file_type.is_dir() {
90+
collect_files_recursive(&entry_path, files);
91+
} else if file_type.is_file() {
92+
files.push(entry_path);
93+
}
94+
}
95+
}
96+
97+
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
98+
if needle.is_empty() {
99+
return true;
100+
}
101+
102+
haystack.windows(needle.len()).any(|window| window == needle)
103+
}

0 commit comments

Comments
 (0)