fix: resolve CI workflow path and add lint job
This commit is contained in:
215
src/rotation.rs
Normal file
215
src/rotation.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
use crate::vault::Vault;
|
||||||
|
use crate::token::TokenData;
|
||||||
|
use chrono::{DateTime, Utc, Duration};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RotationSchedule {
|
||||||
|
pub token_name: String,
|
||||||
|
pub rotation_days: u32,
|
||||||
|
pub last_rotated: Option<DateTime<Utc>>,
|
||||||
|
pub next_rotation: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RotationSchedule {
|
||||||
|
pub fn from_token(token_name: &str, token_data: &TokenData) -> Option<Self> {
|
||||||
|
if !token_data.auto_rotate {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_rotated = token_data.last_rotated.unwrap_or(token_data.created_at);
|
||||||
|
let next_rotation = Some(last_rotated + Duration::days(token_data.rotation_days.unwrap_or(30) as i64));
|
||||||
|
|
||||||
|
Some(RotationSchedule {
|
||||||
|
token_name: token_name.to_string(),
|
||||||
|
rotation_days: token_data.rotation_days.unwrap_or(30),
|
||||||
|
last_rotated: Some(last_rotated),
|
||||||
|
next_rotation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_due(&self) -> bool {
|
||||||
|
if let Some(next) = self.next_rotation {
|
||||||
|
Utc::now() >= next
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn days_until_rotation(&self) -> Option<i64> {
|
||||||
|
self.next_rotation.map(|next| {
|
||||||
|
let diff = next - Utc::now();
|
||||||
|
diff.num_days()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RotationEngine;
|
||||||
|
|
||||||
|
impl RotationEngine {
|
||||||
|
pub fn check_schedules(vault: &Vault) -> Vec<RotationSchedule> {
|
||||||
|
let mut schedules = Vec::new();
|
||||||
|
|
||||||
|
for (name, token_data) in &vault.tokens {
|
||||||
|
if let Some(schedule) = RotationSchedule::from_token(name, token_data) {
|
||||||
|
schedules.push(schedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schedules
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_due_rotations(vault: &Vault) -> Vec<RotationSchedule> {
|
||||||
|
Self::check_schedules(vault)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.is_due())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_upcoming_rotations(vault: &Vault, days: i64) -> Vec<RotationSchedule> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let threshold = now + Duration::days(days);
|
||||||
|
|
||||||
|
Self::check_schedules(vault)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| {
|
||||||
|
if let Some(next) = s.next_rotation {
|
||||||
|
next >= now && next <= threshold
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_due_tokens(vault: &mut Vault) -> HashMap<String, String> {
|
||||||
|
let due_names: Vec<String> = vault.tokens.iter()
|
||||||
|
.filter(|(_, token)| token.should_rotate())
|
||||||
|
.map(|(name, _)| name.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut results = HashMap::new();
|
||||||
|
|
||||||
|
for name in due_names {
|
||||||
|
if let Ok(new_value) = vault.rotate_token(&name, true) {
|
||||||
|
results.insert(name, new_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_next_rotation(token_data: &TokenData) -> Option<DateTime<Utc>> {
|
||||||
|
let base_time = token_data.last_rotated.unwrap_or(token_data.created_at);
|
||||||
|
let days = token_data.rotation_days.unwrap_or(30);
|
||||||
|
Some(base_time + Duration::days(days as i64))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_schedule_report(schedules: &[RotationSchedule]) -> String {
|
||||||
|
if schedules.is_empty() {
|
||||||
|
return "No scheduled rotations".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut report = String::from("Rotation Schedule:\n");
|
||||||
|
report.push_str(&"=".repeat(50));
|
||||||
|
report.push('\n');
|
||||||
|
|
||||||
|
let mut due_count = 0;
|
||||||
|
let mut upcoming_count = 0;
|
||||||
|
|
||||||
|
for schedule in schedules {
|
||||||
|
let status = if schedule.is_due() {
|
||||||
|
due_count += 1;
|
||||||
|
"DUE NOW"
|
||||||
|
} else if let Some(days) = schedule.days_until_rotation() {
|
||||||
|
upcoming_count += 1;
|
||||||
|
format!("in {} days", days)
|
||||||
|
} else {
|
||||||
|
"unknown".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
report.push_str(&format!(
|
||||||
|
" {}: every {} days - {}\n",
|
||||||
|
schedule.token_name,
|
||||||
|
schedule.rotation_days,
|
||||||
|
status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
report.push_str(&"-".repeat(50));
|
||||||
|
report.push('\n');
|
||||||
|
report.push_str(&format!("Due now: {}, Upcoming: {}\n", due_count, upcoming_count));
|
||||||
|
|
||||||
|
report
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::vault::Vault;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rotation_schedule_creation() {
|
||||||
|
let password = "test";
|
||||||
|
let project = "test_schedule";
|
||||||
|
|
||||||
|
let mut vault = Vault::initialize(password, project).unwrap();
|
||||||
|
vault.generate_token("auto_rotate", 32).unwrap();
|
||||||
|
vault.set_rotation("auto_rotate", 30).unwrap();
|
||||||
|
|
||||||
|
let schedules = RotationEngine::check_schedules(&vault);
|
||||||
|
assert_eq!(schedules.len(), 1);
|
||||||
|
assert_eq!(schedules[0].token_name, "auto_rotate");
|
||||||
|
assert_eq!(schedules[0].rotation_days, 30);
|
||||||
|
|
||||||
|
let vault_path = Vault::get_vault_path(project).unwrap();
|
||||||
|
if vault_path.exists() {
|
||||||
|
std::fs::remove_file(&vault_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_manual_rotation_not_scheduled() {
|
||||||
|
let password = "test";
|
||||||
|
let project = "test_manual";
|
||||||
|
|
||||||
|
let mut vault = Vault::initialize(password, project).unwrap();
|
||||||
|
vault.generate_token("manual", 32).unwrap();
|
||||||
|
|
||||||
|
let schedules = RotationEngine::check_schedules(&vault);
|
||||||
|
assert!(schedules.is_empty());
|
||||||
|
|
||||||
|
let vault_path = Vault::get_vault_path(project).unwrap();
|
||||||
|
if vault_path.exists() {
|
||||||
|
std::fs::remove_file(&vault_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_schedule_report() {
|
||||||
|
let schedules = vec![
|
||||||
|
RotationSchedule {
|
||||||
|
token_name: "token1".to_string(),
|
||||||
|
rotation_days: 30,
|
||||||
|
last_rotated: Some(Utc::now() - Duration::days(35)),
|
||||||
|
next_rotation: Some(Utc::now() - Duration::days(5)),
|
||||||
|
},
|
||||||
|
RotationSchedule {
|
||||||
|
token_name: "token2".to_string(),
|
||||||
|
rotation_days: 60,
|
||||||
|
last_rotated: Some(Utc::now()),
|
||||||
|
next_rotation: Some(Utc::now() + Duration::days(60)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let report = RotationEngine::format_schedule_report(&schedules);
|
||||||
|
assert!(report.contains("token1"));
|
||||||
|
assert!(report.contains("token2"));
|
||||||
|
assert!(report.contains("DUE NOW"));
|
||||||
|
assert!(report.contains("in 60 days"));
|
||||||
|
|
||||||
|
let empty_report = RotationEngine::format_schedule_report(&[]);
|
||||||
|
assert_eq!(empty_report, "No scheduled rotations");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user