Skip to content

Commit 94abfdf

Browse files
authored
SGA-12959- sync satori credentials and config using satori as source … (#146)
* SGA-12959- sync satori credentials and config using satori as source of truth and updating the user aws account * SGA-12959- fix old errors found uisng clippy * SGA-12959- add audit package * SGA-12959- add audit package ,fix typo * SGA-12959- add tests
1 parent 2a460eb commit 94abfdf

File tree

9 files changed

+227
-54
lines changed

9 files changed

+227
-54
lines changed

.github/workflows/rust.yml

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Rust
22

33
on:
44
push:
5-
branches: [ "main" ]
5+
branches: ["main"]
66
pull_request:
7-
branches: [ "main" ]
7+
branches: ["main"]
88

99
env:
1010
CARGO_TERM_COLOR: always
@@ -14,68 +14,67 @@ jobs:
1414
runs-on: ubuntu-latest
1515

1616
steps:
17-
- uses: actions/checkout@v4
18-
- name: Build
19-
run: cargo build --verbose
20-
- name: install next test
21-
run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
22-
- name: Run tests
23-
run: cargo nextest run --verbose
24-
- name: Upload Artifact
25-
uses: actions/upload-artifact@v4
26-
with:
27-
if-no-files-found: error
28-
name: satori_linux
29-
path: ./target/debug/satori
30-
17+
- uses: actions/checkout@v4
18+
- name: Build
19+
run: cargo build --verbose
20+
- name: install next test
21+
run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
22+
- name: Run tests
23+
run: cargo nextest run --verbose
24+
- name: Upload Artifact
25+
uses: actions/upload-artifact@v4
26+
with:
27+
if-no-files-found: error
28+
name: satori_linux
29+
path: ./target/debug/satori
30+
3131
build-windows:
3232
runs-on: windows-latest
3333

3434
steps:
35-
- uses: actions/checkout@v4
36-
- name: Build
37-
run: cargo build --verbose
38-
- name: Upload Artifact
39-
uses: actions/upload-artifact@v4
40-
with:
41-
if-no-files-found: error
42-
name: satori.exe
43-
path: ./target/debug/satori.exe
44-
35+
- uses: actions/checkout@v4
36+
- name: Build
37+
run: cargo build --verbose
38+
- name: Upload Artifact
39+
uses: actions/upload-artifact@v4
40+
with:
41+
if-no-files-found: error
42+
name: satori.exe
43+
path: ./target/debug/satori.exe
44+
4545
build-mac:
4646
runs-on: macos-latest
4747

4848
steps:
49-
- uses: actions/checkout@v4
50-
- name: Build
51-
run: cargo build --verbose
52-
- name: Upload Artifact
53-
uses: actions/upload-artifact@v4
54-
with:
55-
if-no-files-found: error
56-
name: satori_mac
57-
path: ./target/debug/satori
58-
49+
- uses: actions/checkout@v4
50+
- name: Build
51+
run: cargo build --verbose
52+
- name: Upload Artifact
53+
uses: actions/upload-artifact@v4
54+
with:
55+
if-no-files-found: error
56+
name: satori_mac
57+
path: ./target/debug/satori
58+
5959
clippy:
6060
runs-on: ubuntu-latest
61-
steps:
62-
- uses: actions/checkout@v4
63-
- name: Build
64-
run: cargo build --verbose
65-
- name: clippy
66-
run: cargo clippy -- -D warnings
61+
steps:
62+
- uses: actions/checkout@v4
63+
- name: Build
64+
run: cargo build --verbose
65+
- name: clippy
66+
run: cargo clippy -- -D warnings
6767

6868
audit:
6969
runs-on: ubuntu-latest
7070
steps:
7171
- uses: actions/checkout@v4
7272
- name: Audit
73-
run: cargo audit
74-
73+
run: cargo install cargo-audit && cargo audit
74+
7575
fmt:
7676
runs-on: ubuntu-latest
77-
steps:
78-
- uses: actions/checkout@v4
79-
- name: fmt
80-
run: cargo fmt -- --check
81-
77+
steps:
78+
- uses: actions/checkout@v4
79+
- name: fmt
80+
run: cargo fmt -- --check

src/cli/parsers/run/dbt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub fn build(args: &ArgMatches) -> Result<Flow, CliError> {
3838
/// 2. `DBT_PROFILES_DIR` environment variable is set
3939
/// 3. profiles.yml file is found in the current directory
4040
/// 4. default to ~/.dbt directory
41-
/// The file is always named profiles.yml
41+
/// The file is always named profiles.yml
4242
fn get_profiles_path(args: &ArgMatches) -> PathBuf {
4343
match args.get_one::<PathBuf>("profile-dir") {
4444
Some(profile_dir) => Path::new(&profile_dir).to_path_buf(),

src/cli/parsers/tools/aws.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ fn get_from_env_or_default(env_var: &str, default: &str) -> Result<PathBuf, cli:
4242
Err(err) => {
4343
log::debug!("failed to read path from env var: {}", err);
4444

45-
let home_dir =
46-
homedir::get_my_home()?.ok_or_else(|| cli::errors::CliError::HomeDirNotFound)?;
45+
let home_dir = homedir::get_my_home()?.ok_or(cli::errors::CliError::HomeDirNotFound)?;
4746
Ok(home_dir.join(default))
4847
}
4948
}

src/cli/parsers/tools/pgpass.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn build(args: &ArgMatches) -> Result<PgPass, CliError> {
3232
#[cfg(target_family = "unix")]
3333
fn get_pgpass_file_path() -> Result<PathBuf, CliError> {
3434
Ok(homedir::get_my_home()?
35-
.ok_or_else(|| CliError::HomeDirNotFound)?
35+
.ok_or(CliError::HomeDirNotFound)?
3636
.join(Path::new(PGPASS_FILE_NAME)))
3737
}
3838

src/tools/aws/flow.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::hash_map::DefaultHasher,
2+
collections::{hash_map::DefaultHasher, HashSet},
33
hash::{Hash, Hasher},
44
path::Path,
55
};
@@ -28,6 +28,8 @@ where
2828
let (credentials, datastores_info) =
2929
login::run_with_file(&params.login, user_input_stream).await?;
3030

31+
let mut expected_satori_profiles = HashSet::new();
32+
3133
let mut is_first = true;
3234
for (datastore_name, datastore_info) in get_aws_datastores(&datastores_info) {
3335
let datastore_type = format!("{:?}", &datastore_info.r#type);
@@ -38,6 +40,8 @@ where
3840
);
3941
let endpoint_url = &datastore_info.get_datastore_name()?;
4042

43+
expected_satori_profiles.insert(profile_name.clone());
44+
4145
config_content
4246
.with_section(Some(format!("profile {profile_name}")))
4347
.set("endpoint_url", format!("https://{endpoint_url}"));
@@ -53,6 +57,9 @@ where
5357
log::info!(" {datastore_name}: {profile_name}");
5458
}
5559

60+
remove_stale_satori_profiles(&mut credentials_content, &expected_satori_profiles);
61+
remove_stale_satori_profiles_from_config(&mut config_content, &expected_satori_profiles);
62+
5663
credentials_content
5764
.write_to_file(params.credentials_path.clone())
5865
.map_err(|err| errors::ToolsError::FailedToWriteToFile(err, params.credentials_path))?;
@@ -98,6 +105,55 @@ fn get_hash_for_datastore(datastore_info: &DatastoreInfo, num_digits: u32) -> u6
98105
hash_value % divisor
99106
}
100107

108+
fn remove_stale_satori_profiles(ini: &mut Ini, expected_profiles: &HashSet<String>) {
109+
let sections_to_remove: Vec<String> = ini
110+
.sections()
111+
.filter_map(|section_name| {
112+
section_name.and_then(|name| {
113+
if name.starts_with(PROFILE_NAME_PREFIX) && !expected_profiles.contains(name) {
114+
Some(name.to_string())
115+
} else {
116+
None
117+
}
118+
})
119+
})
120+
.collect();
121+
122+
for section in &sections_to_remove {
123+
log::info!("Removing stale profile: {}", section);
124+
ini.delete(Some(section));
125+
}
126+
}
127+
128+
fn remove_stale_satori_profiles_from_config(ini: &mut Ini, expected_profiles: &HashSet<String>) {
129+
let prefix = format!("profile {}", PROFILE_NAME_PREFIX);
130+
let sections_to_remove: Vec<String> = ini
131+
.sections()
132+
.filter_map(|section_name| {
133+
section_name.and_then(|name| {
134+
if name.starts_with(&prefix) {
135+
// Extract profile name without "profile " prefix
136+
let profile_name = name.strip_prefix("profile ").unwrap_or(name);
137+
if profile_name.starts_with(PROFILE_NAME_PREFIX)
138+
&& !expected_profiles.contains(profile_name)
139+
{
140+
Some(name.to_string())
141+
} else {
142+
None
143+
}
144+
} else {
145+
None
146+
}
147+
})
148+
})
149+
.collect();
150+
151+
for section in &sections_to_remove {
152+
log::info!("Removing stale config section: {}", section);
153+
ini.delete(Some(section));
154+
}
155+
}
156+
101157
#[cfg(test)]
102158
mod tests {
103159
use std::collections::HashMap;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[default]
2+
region=us-east-1
3+
4+
[profile satori_athena_190337]
5+
endpoint_url=https://athena.example.com
6+
7+
[profile satori_s3_575495]
8+
endpoint_url=https://s3.example.com
9+
10+
[profile personal-account]
11+
region=us-west-2
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[default]
2+
aws_access_key_id=USER_PERSONAL
3+
aws_secret_access_key=PERSONAL_PASSWORD
4+
5+
[satori_athena_190337]
6+
aws_access_key_id=USER
7+
aws_secret_access_key=OLD_PASSWORD
8+
9+
[satori_s3_575495]
10+
aws_access_key_id=USER
11+
aws_secret_access_key=OLD_PASSWORD
12+
13+
[personal-account]
14+
aws_access_key_id=PERSONAL_KEY
15+
aws_secret_access_key=PERSONAL_SECRET
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"account_id": "account_id",
3+
"datastores": {
4+
"athena1": {
5+
"satori_host": "athena.example.com",
6+
"databases": [],
7+
"port": null,
8+
"type": "ATHENA",
9+
"deployment_type": null
10+
}
11+
}
12+
}

tests/test_aws.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,87 @@ async fn test_aws_expired_credentials() {
141141
validates_aws_files(&temp_dir, &credentials, &datastores_info);
142142
}
143143

144+
/// Test that stale Satori profiles are removed when user loses access to datastores
145+
/// User had S3 and Athena, now only has Athena. S3 profile should be removed.
146+
#[tokio::test]
147+
async fn test_aws_stale_profiles_removed() {
148+
let temp_dir = temp_dir::generate();
149+
let credentials = get_old_credentials_expire_two_hours();
150+
let datastores_info = get_mock_datastores("athena_only_datastores.json");
151+
let datastores_entries_response_path = get_access_details_db_empty_response_path();
152+
153+
// Start with files that have both S3 and Athena profiles
154+
let old_config = read_ini_file(AWS_CREDENTIALS_DIR, "stale_config_with_s3");
155+
let old_credentials = read_ini_file(AWS_CREDENTIALS_DIR, "stale_credentials_with_s3");
156+
157+
write_credentials_temp_dir(&credentials, &temp_dir);
158+
write_datastores_temp_dir(&datastores_info, &temp_dir);
159+
write_aws_temp_dir(&temp_dir, old_config, old_credentials);
160+
161+
run_aws_with_server_assert_no_calls_to_server(
162+
&temp_dir,
163+
&datastores_entries_response_path,
164+
AwsBuilder::default(),
165+
)
166+
.await;
167+
168+
// Read the resulting files
169+
let actual_credentials = read_actual_aws_file(&temp_dir, "credentials");
170+
let actual_config = read_actual_aws_file(&temp_dir, "config");
171+
172+
// Verify S3 profile was removed from credentials
173+
assert!(
174+
actual_credentials.section(Some(S3_PROFILE)).is_none(),
175+
"S3 profile should be removed from credentials"
176+
);
177+
178+
// Verify S3 profile was removed from config
179+
let s3_config_section = format!("profile {}", S3_PROFILE);
180+
assert!(
181+
actual_config.section(Some(&s3_config_section)).is_none(),
182+
"S3 profile should be removed from config"
183+
);
184+
185+
// Verify Athena profile still exists in credentials
186+
assert!(
187+
actual_credentials.section(Some(ATHENA_PROFILE)).is_some(),
188+
"Athena profile should still exist in credentials"
189+
);
190+
191+
// Verify Athena profile still exists in config
192+
let athena_config_section = format!("profile {}", ATHENA_PROFILE);
193+
assert!(
194+
actual_config
195+
.section(Some(&athena_config_section))
196+
.is_some(),
197+
"Athena profile should still exist in config"
198+
);
199+
200+
// Verify non-Satori profiles are preserved in credentials
201+
assert!(
202+
actual_credentials.section(Some("default")).is_some(),
203+
"Default profile should be preserved"
204+
);
205+
assert!(
206+
actual_credentials
207+
.section(Some("personal-account"))
208+
.is_some(),
209+
"Personal account profile should be preserved"
210+
);
211+
212+
// Verify non-Satori profiles are preserved in config
213+
assert!(
214+
actual_config.section(Some("default")).is_some(),
215+
"Default config should be preserved"
216+
);
217+
assert!(
218+
actual_config
219+
.section(Some("profile personal-account"))
220+
.is_some(),
221+
"Personal account config should be preserved"
222+
);
223+
}
224+
144225
async fn run_aws_with_server_assert_no_calls_to_server(
145226
temp_dir: &TempDir,
146227
datastores_info_file_path: &Path,

0 commit comments

Comments
 (0)