fix(security): durcir les secrets locaux et valider les credentials
closes #4
This commit is contained in:
parent
8d0b345751
commit
91459c16cc
3 changed files with 225 additions and 12 deletions
|
|
@ -12,6 +12,10 @@ pub fn set_tuleap_credentials(
|
|||
username: String,
|
||||
password: String,
|
||||
) -> Result<TuleapCredentialsSafe, AppError> {
|
||||
let (tuleap_url, username, password) =
|
||||
TuleapCredentials::validate_input(&tuleap_url, &username, &password)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let password_encrypted =
|
||||
crypto::encrypt(&state.encryption_key, &password).map_err(AppError::from)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -113,18 +113,150 @@ pub fn run() {
|
|||
|
||||
fn load_or_generate_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std::error::Error>> {
|
||||
use rand::RngCore;
|
||||
|
||||
if path.exists() {
|
||||
let bytes = std::fs::read(path)?;
|
||||
if bytes.len() != 32 {
|
||||
return Err("Invalid key file size".into());
|
||||
}
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&bytes);
|
||||
Ok(key)
|
||||
} else {
|
||||
let mut key = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut key);
|
||||
std::fs::write(path, key)?;
|
||||
Ok(key)
|
||||
return read_key(path);
|
||||
}
|
||||
|
||||
let mut key = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut key);
|
||||
|
||||
match write_new_key(path, &key) {
|
||||
Ok(()) => Ok(key),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => read_key(path),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std::error::Error>> {
|
||||
enforce_key_permissions(path)?;
|
||||
|
||||
let bytes = std::fs::read(path)?;
|
||||
if bytes.len() != 32 {
|
||||
return Err("Invalid key file size".into());
|
||||
}
|
||||
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&bytes);
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
fn write_new_key(path: &std::path::Path, key: &[u8; 32]) -> std::io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.mode(0o600)
|
||||
.open(path)?;
|
||||
file.write_all(key)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
std::fs::write(path, key)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enforce_key_permissions(path: &std::path::Path) -> std::io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let current_mode = metadata.permissions().mode() & 0o777;
|
||||
if current_mode != 0o600 {
|
||||
let mut permissions = metadata.permissions();
|
||||
permissions.set_mode(0o600);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = path;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_load_or_generate_key_creates_key() {
|
||||
let dir = tempdir().expect("tempdir should be created");
|
||||
let path = dir.path().join("orchai.key");
|
||||
|
||||
let key = load_or_generate_key(&path).expect("key generation should succeed");
|
||||
assert_eq!(key.len(), 32);
|
||||
|
||||
let persisted = fs::read(&path).expect("key file should exist");
|
||||
assert_eq!(persisted.len(), 32);
|
||||
assert_eq!(persisted, key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_or_generate_key_reads_existing_key() {
|
||||
let dir = tempdir().expect("tempdir should be created");
|
||||
let path = dir.path().join("orchai.key");
|
||||
let expected = [42u8; 32];
|
||||
fs::write(&path, expected).expect("existing key should be written");
|
||||
|
||||
let loaded = load_or_generate_key(&path).expect("existing key should be loaded");
|
||||
assert_eq!(loaded, expected);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_load_or_generate_key_creates_private_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().expect("tempdir should be created");
|
||||
let path = dir.path().join("orchai.key");
|
||||
|
||||
load_or_generate_key(&path).expect("key generation should succeed");
|
||||
|
||||
let mode = fs::metadata(&path)
|
||||
.expect("metadata should be readable")
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777;
|
||||
assert_eq!(mode, 0o600);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_load_or_generate_key_hardens_existing_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempdir().expect("tempdir should be created");
|
||||
let path = dir.path().join("orchai.key");
|
||||
fs::write(&path, [7u8; 32]).expect("existing key should be written");
|
||||
|
||||
let mut permissions = fs::metadata(&path)
|
||||
.expect("metadata should be readable")
|
||||
.permissions();
|
||||
permissions.set_mode(0o644);
|
||||
fs::set_permissions(&path, permissions).expect("permissions should be set");
|
||||
|
||||
load_or_generate_key(&path).expect("existing key should be loaded");
|
||||
|
||||
let mode = fs::metadata(&path)
|
||||
.expect("metadata should be readable")
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777;
|
||||
assert_eq!(mode, 0o600);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,39 @@ pub struct TuleapCredentialsSafe {
|
|||
}
|
||||
|
||||
impl TuleapCredentials {
|
||||
pub fn validate_input(
|
||||
tuleap_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> std::result::Result<(String, String, String), String> {
|
||||
let normalized_url = tuleap_url.trim().trim_end_matches('/').to_string();
|
||||
if normalized_url.is_empty() {
|
||||
return Err("Tuleap URL cannot be empty".to_string());
|
||||
}
|
||||
|
||||
let parsed_url = reqwest::Url::parse(&normalized_url)
|
||||
.map_err(|_| "Tuleap URL must be a valid absolute URL".to_string())?;
|
||||
|
||||
if !matches!(parsed_url.scheme(), "http" | "https") {
|
||||
return Err("Tuleap URL must use http or https".to_string());
|
||||
}
|
||||
|
||||
if parsed_url.host_str().is_none() {
|
||||
return Err("Tuleap URL must include a host".to_string());
|
||||
}
|
||||
|
||||
let normalized_username = username.trim().to_string();
|
||||
if normalized_username.is_empty() {
|
||||
return Err("Username cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if password.trim().is_empty() {
|
||||
return Err("Password cannot be empty".to_string());
|
||||
}
|
||||
|
||||
Ok((normalized_url, normalized_username, password.to_string()))
|
||||
}
|
||||
|
||||
pub fn upsert(
|
||||
conn: &Connection,
|
||||
tuleap_url: &str,
|
||||
|
|
@ -154,4 +187,48 @@ mod tests {
|
|||
let result = TuleapCredentials::get(&conn).expect("get should succeed");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_input_trims_url_and_username() {
|
||||
let (url, username, password) =
|
||||
TuleapCredentials::validate_input(" https://tuleap.example.com/ ", " alice ", "secret")
|
||||
.expect("validation should succeed");
|
||||
|
||||
assert_eq!(url, "https://tuleap.example.com");
|
||||
assert_eq!(username, "alice");
|
||||
assert_eq!(password, "secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_input_rejects_empty_url() {
|
||||
let result = TuleapCredentials::validate_input(" ", "alice", "secret");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_input_rejects_invalid_url() {
|
||||
let result = TuleapCredentials::validate_input("not-an-url", "alice", "secret");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_input_rejects_non_http_scheme() {
|
||||
let result =
|
||||
TuleapCredentials::validate_input("ftp://tuleap.example.com", "alice", "secret");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_input_rejects_empty_username() {
|
||||
let result =
|
||||
TuleapCredentials::validate_input("https://tuleap.example.com", " ", "secret");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_input_rejects_empty_password() {
|
||||
let result =
|
||||
TuleapCredentials::validate_input("https://tuleap.example.com", "alice", " ");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue