pub mod secrets;
pub mod ui;
pub mod yaml;
use anyhow::Context;
use serde::Serialize;
use std::path::Path;
use tokio::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
pub async fn write_secure(path: &Path, content: &str) -> anyhow::Result<()> {
#[cfg(unix)]
{
use tokio::io::AsyncWriteExt;
if fs::try_exists(path).await.unwrap_or(false) {
fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.await
.with_context(|| format!("Failed to set permissions for {:?}", path))?;
}
let mut options = fs::OpenOptions::new();
options.write(true).create(true).truncate(true).mode(0o600);
let mut file = options
.open(path)
.await
.with_context(|| format!("Failed to open {:?}", path))?;
file.write_all(content.as_bytes())
.await
.with_context(|| format!("Failed to write to {:?}", path))?;
file.flush()
.await
.with_context(|| format!("Failed to flush {:?}", path))?;
}
#[cfg(not(unix))]
{
fs::write(path, content)
.await
.with_context(|| format!("Failed to write {:?}", path))?;
}
Ok(())
}
pub fn recursive_sort(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for (_, v) in map.iter_mut() {
recursive_sort(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr.iter_mut() {
recursive_sort(v);
}
if arr.is_empty() {
return;
}
if arr
.iter()
.all(|v| v.is_string() || v.is_number() || v.is_boolean())
{
arr.sort_by_cached_key(|a| a.to_string());
} else if arr.iter().all(|v| v.is_object()) {
let keys = ["id", "clientId", "username", "alias", "name"];
for key in keys {
if arr.iter().all(|v| v.get(key).is_some()) {
arr.sort_by_cached_key(|a| {
a.get(key).map_or(String::new(), |v| v.to_string())
});
break;
}
}
}
}
_ => {}
}
}
pub fn to_sorted_yaml_with_secrets<T: Serialize>(
value: &T,
prefix: &str,
secrets: &mut std::collections::BTreeMap<String, String>,
) -> anyhow::Result<String> {
let mut json_value =
serde_json::to_value(value).context("Failed to serialize to JSON value")?;
crate::utils::secrets::extract_secrets(&mut json_value, prefix, secrets);
recursive_sort(&mut json_value);
serde_yaml::to_string(&json_value).context("Failed to serialize to sorted YAML")
}
pub fn to_sorted_yaml<T: Serialize>(value: &T) -> anyhow::Result<String> {
let mut json_value =
serde_json::to_value(value).context("Failed to serialize to JSON value")?;
recursive_sort(&mut json_value);
serde_yaml::to_string(&json_value).context("Failed to serialize to sorted YAML")
}
pub async fn join_all_tasks<T: 'static>(
mut set: tokio::task::JoinSet<anyhow::Result<T>>,
context_msg: Option<&str>,
) -> anyhow::Result<Vec<T>> {
let mut results = Vec::new();
while let Some(res) = set.join_next().await {
if let Some(msg) = context_msg {
results.push(res.context(msg.to_string())??);
} else {
results.push(res??);
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sorting_with_value() {
let val = serde_json::json!({
"z": "val_z",
"a": "val_a",
"m": ["item3", "item1", "item2"]
});
let yaml = to_sorted_yaml(&val).expect("Failed to serialize yaml");
println!("Generated YAML:\n{}", yaml);
let lines: Vec<&str> = yaml.lines().collect();
assert_eq!(lines[0], "a: val_a");
assert_eq!(lines[1], "m:");
assert_eq!(lines[2], "- item1");
assert_eq!(lines[3], "- item2");
assert_eq!(lines[4], "- item3");
assert_eq!(lines[5], "z: val_z");
}
#[test]
fn test_sorting_arrays_of_objects() {
let val = serde_json::json!({
"list": [
{ "name": "c", "v": 3 },
{ "name": "a", "v": 1 },
{ "name": "b", "v": 2 }
],
"aliases": [
{ "alias": "z" },
{ "alias": "x" }
],
"ids": [
{ "id": "2" },
{ "id": "1" }
]
});
let yaml = to_sorted_yaml(&val).expect("Failed to serialize yaml");
println!("Generated YAML:\n{}", yaml);
let lines: Vec<&str> = yaml.lines().collect();
assert_eq!(lines[0], "aliases:");
assert_eq!(lines[1], "- alias: x");
assert_eq!(lines[2], "- alias: z");
assert_eq!(lines[3], "ids:");
assert_eq!(lines[4], "- id: '1'");
assert_eq!(lines[5], "- id: '2'");
assert_eq!(lines[6], "list:");
assert_eq!(lines[7], "- name: a");
assert_eq!(lines[8], " v: 1");
assert_eq!(lines[9], "- name: b");
assert_eq!(lines[10], " v: 2");
assert_eq!(lines[11], "- name: c");
assert_eq!(lines[12], " v: 3");
}
#[test]
fn test_recursive_sort_empty_array() {
let mut val = serde_json::json!([]);
recursive_sort(&mut val);
assert_eq!(val, serde_json::json!([]));
let mut val_obj = serde_json::json!({ "empty_arr": [] });
recursive_sort(&mut val_obj);
assert_eq!(val_obj, serde_json::json!({ "empty_arr": [] }));
}
#[test]
fn test_recursive_sort_simple_arrays() {
let mut val = serde_json::json!([3, 1, 2]);
recursive_sort(&mut val);
assert_eq!(val, serde_json::json!([1, 2, 3]));
let mut val_bool = serde_json::json!([true, false, true]);
recursive_sort(&mut val_bool);
assert_eq!(val_bool, serde_json::json!([false, true, true]));
let mut val_mixed = serde_json::json!(["b", 1, true, false, "a"]);
recursive_sort(&mut val_mixed);
assert_eq!(val_mixed, serde_json::json!(["a", "b", 1, false, true]));
}
#[test]
fn test_recursive_sort_mixed_and_no_keys() {
let mut val_mixed = serde_json::json!([{"a": 1}, 1, "string"]);
recursive_sort(&mut val_mixed);
assert_eq!(val_mixed, serde_json::json!([{"a": 1}, 1, "string"]));
let mut val_no_keys = serde_json::json!([
{"other": 2},
{"other": 1}
]);
recursive_sort(&mut val_no_keys);
assert_eq!(
val_no_keys,
serde_json::json!([
{"other": 2},
{"other": 1}
])
);
}
#[test]
fn test_recursive_sort_nested_arrays() {
let mut val = serde_json::json!({
"nested": [[2, 1], [4, 3]]
});
recursive_sort(&mut val);
assert_eq!(
val,
serde_json::json!({
"nested": [[1, 2], [3, 4]]
})
);
}
#[test]
fn test_recursive_sort_primitive() {
let mut val_str = serde_json::json!("test");
recursive_sort(&mut val_str);
assert_eq!(val_str, serde_json::json!("test"));
let mut val_num = serde_json::json!(42);
recursive_sort(&mut val_num);
assert_eq!(val_num, serde_json::json!(42));
let mut val_null = serde_json::Value::Null;
recursive_sort(&mut val_null);
assert_eq!(val_null, serde_json::Value::Null);
}
#[test]
fn test_recursive_sort_mixed_identify_keys() {
let mut val1 = serde_json::json!([
{ "id": "2", "name": "a" },
{ "id": "1", "name": "b" }
]);
recursive_sort(&mut val1);
assert_eq!(
val1,
serde_json::json!([
{ "id": "1", "name": "b" },
{ "id": "2", "name": "a" }
])
);
let mut val2 = serde_json::json!([
{ "id": "1", "name": "b" },
{ "name": "a" }
]);
recursive_sort(&mut val2);
assert_eq!(
val2,
serde_json::json!([
{ "name": "a" },
{ "id": "1", "name": "b" }
])
);
let mut val3 = serde_json::json!([
{ "alias": "z", "name": "a" },
{ "alias": "x", "id": "1" }
]);
recursive_sort(&mut val3);
assert_eq!(
val3,
serde_json::json!([
{ "alias": "x", "id": "1" },
{ "alias": "z", "name": "a" }
])
);
}
#[test]
fn test_to_sorted_yaml_with_secrets() {
let mut secrets = std::collections::BTreeMap::new();
let val = serde_json::json!({
"clientId": "myclient",
"secret": "very-secret",
"nested": {
"password": "pass"
}
});
let yaml = to_sorted_yaml_with_secrets(&val, "CLIENT", &mut secrets).unwrap();
assert!(yaml.contains("secret: ${KEYCLOAK_CLIENT_MYCLIENT_SECRET}"));
assert!(yaml.contains("password: ${KEYCLOAK_CLIENT_MYCLIENT_NESTED_PASSWORD}"));
assert_eq!(
secrets.get("KEYCLOAK_CLIENT_MYCLIENT_SECRET"),
Some(&"very-secret".to_string())
);
assert_eq!(
secrets.get("KEYCLOAK_CLIENT_MYCLIENT_NESTED_PASSWORD"),
Some(&"pass".to_string())
);
}
#[test]
fn test_to_sorted_yaml_with_secrets_struct() {
#[derive(serde::Serialize)]
struct TestConfig {
#[serde(rename = "clientId")]
client_id: String,
name: String,
password: Option<String>,
value: i32,
}
let mut secrets = std::collections::BTreeMap::new();
let val = TestConfig {
client_id: "test-client".to_string(),
name: "test-name".to_string(),
password: Some("super-secret".to_string()),
value: 42,
};
let yaml = to_sorted_yaml_with_secrets(&val, "APP", &mut secrets).unwrap();
assert!(yaml.contains("password: ${KEYCLOAK_APP_TEST_CLIENT_PASSWORD}"));
assert!(yaml.contains("clientId: test-client"));
assert_eq!(
secrets.get("KEYCLOAK_APP_TEST_CLIENT_PASSWORD"),
Some(&"super-secret".to_string())
);
}
#[test]
fn test_to_sorted_yaml_with_secrets_error() {
struct FailingStruct;
impl serde::Serialize for FailingStruct {
fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("Simulated serialization error"))
}
}
let mut secrets = std::collections::BTreeMap::new();
let result = to_sorted_yaml_with_secrets(&FailingStruct, "FAIL", &mut secrets);
assert!(result.is_err());
}
#[test]
fn test_to_sorted_yaml_simple() {
let val = serde_json::json!({ "b": 2, "a": 1 });
let yaml = to_sorted_yaml(&val).unwrap();
assert_eq!(yaml.trim(), "a: 1\nb: 2");
}
#[tokio::test]
async fn test_write_secure_permissions() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("secure.txt");
let content = "sensitive data";
write_secure(&file_path, content).await.unwrap();
let read_content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(read_content, content);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&file_path).unwrap();
let mode = metadata.permissions().mode();
assert_eq!(mode & 0o777, 0o600);
}
let existing_path = temp_dir.path().join("existing.txt");
std::fs::write(&existing_path, "old content").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&existing_path, std::fs::Permissions::from_mode(0o644))
.unwrap();
let metadata = std::fs::metadata(&existing_path).unwrap();
assert_eq!(metadata.permissions().mode() & 0o777, 0o644);
}
write_secure(&existing_path, "new content").await.unwrap();
let read_content = std::fs::read_to_string(&existing_path).unwrap();
assert_eq!(read_content, "new content");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&existing_path).unwrap();
let mode = metadata.permissions().mode();
assert_eq!(mode & 0o777, 0o600);
}
}
#[test]
fn test_recursive_sort_keycloak_identifiers() {
let mut val_client = serde_json::json!([
{ "clientId": "z", "name": "app1" },
{ "clientId": "a", "name": "app2" }
]);
recursive_sort(&mut val_client);
assert_eq!(
val_client,
serde_json::json!([
{ "clientId": "a", "name": "app2" },
{ "clientId": "z", "name": "app1" }
])
);
let mut val_user = serde_json::json!([
{ "username": "user2" },
{ "username": "user1" }
]);
recursive_sort(&mut val_user);
assert_eq!(
val_user,
serde_json::json!([
{ "username": "user1" },
{ "username": "user2" }
])
);
let mut val_nested = serde_json::json!({
"users": [
{ "username": "user2" },
{ "username": "user1" }
],
"config": {
"clients": [
{ "clientId": "z" },
{ "clientId": "a" }
]
}
});
recursive_sort(&mut val_nested);
assert_eq!(
val_nested,
serde_json::json!({
"users": [
{ "username": "user1" },
{ "username": "user2" }
],
"config": {
"clients": [
{ "clientId": "a" },
{ "clientId": "z" }
]
}
})
);
}
#[test]
fn test_recursive_sort_edge_cases() {
let mut val_empty_objs = serde_json::json!([{}, {}]);
recursive_sort(&mut val_empty_objs);
assert_eq!(val_empty_objs, serde_json::json!([{}, {}]));
let mut val_deep_arrays = serde_json::json!([[[2, 1]], [[4, 3]]]);
recursive_sort(&mut val_deep_arrays);
assert_eq!(val_deep_arrays, serde_json::json!([[[1, 2]], [[3, 4]]]));
let mut val_mixed_null = serde_json::json!([1, null, "a"]);
recursive_sort(&mut val_mixed_null);
assert_eq!(val_mixed_null, serde_json::json!([1, null, "a"]));
let mut val_int_keys = serde_json::json!([
{ "id": 2, "val": "b" },
{ "id": 1, "val": "a" },
{ "id": 10, "val": "c" }
]);
recursive_sort(&mut val_int_keys);
assert_eq!(
val_int_keys,
serde_json::json!([
{ "id": 1, "val": "a" },
{ "id": 10, "val": "c" },
{ "id": 2, "val": "b" }
])
);
let mut val_null_keys = serde_json::json!([
{ "id": null, "val": "b" },
{ "id": 1, "val": "a" }
]);
recursive_sort(&mut val_null_keys);
assert_eq!(
val_null_keys,
serde_json::json!([
{ "id": 1, "val": "a" },
{ "id": null, "val": "b" }
])
);
}
}