use std::path::Path;
use anyhow::Context;
use serde::de::DeserializeOwned;
use serde::Serialize;
use tokio::fs::{self, OpenOptions};
use tokio::io::AsyncWriteExt;
use crate::AppState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum GovernanceStoreFile {
ProtectedAudit,
MemoryAudit,
PolicyDecisions,
OrgUnits,
OrgUnitMemberships,
OrgUnitAccessGrants,
CrossTenantGrants,
SourceBindings,
PolicyRules,
}
pub(crate) struct GovernanceStore<'a> {
state: &'a AppState,
}
pub(crate) fn for_state(state: &AppState) -> GovernanceStore<'_> {
GovernanceStore { state }
}
impl<'a> GovernanceStore<'a> {
fn path(&self, file: GovernanceStoreFile) -> &Path {
match file {
GovernanceStoreFile::ProtectedAudit => &self.state.protected_audit_path,
GovernanceStoreFile::MemoryAudit => &self.state.memory_audit_path,
GovernanceStoreFile::PolicyDecisions => &self.state.policy_decisions_path,
GovernanceStoreFile::OrgUnits => &self.state.enterprise.org_units_path,
GovernanceStoreFile::OrgUnitMemberships => {
&self.state.enterprise.org_unit_memberships_path
}
GovernanceStoreFile::OrgUnitAccessGrants => {
&self.state.enterprise.org_unit_access_grants_path
}
GovernanceStoreFile::CrossTenantGrants => {
&self.state.enterprise.cross_tenant_grants_path
}
GovernanceStoreFile::SourceBindings => &self.state.enterprise.source_bindings_path,
GovernanceStoreFile::PolicyRules => &self.state.enterprise.policy_rules_path,
}
}
fn is_jsonl(file: GovernanceStoreFile) -> bool {
matches!(
file,
GovernanceStoreFile::ProtectedAudit | GovernanceStoreFile::MemoryAudit
)
}
pub(crate) async fn read_text(
&self,
file: GovernanceStoreFile,
) -> anyhow::Result<Option<String>> {
let path = self.path(file);
if Self::is_jsonl(file) {
return match fs::read_to_string(path).await {
Ok(content) => Ok(Some(content)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => {
Err(err).with_context(|| format!("read governance store {}", path.display()))
}
};
}
match crate::encrypted_file_store::read_text_file(path).await {
Ok(content) => Ok(Some(content)),
Err(err) => match err.downcast_ref::<std::io::Error>() {
Some(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => Ok(None),
_ => Err(err).with_context(|| format!("read governance store {}", path.display())),
},
}
}
pub(crate) async fn read_json<T>(&self, file: GovernanceStoreFile) -> anyhow::Result<Option<T>>
where
T: DeserializeOwned,
{
let Some(content) = self.read_text(file).await? else {
return Ok(None);
};
let path = self.path(file);
let parsed = serde_json::from_str(&content)
.with_context(|| format!("parse governance store JSON {}", path.display()))?;
Ok(Some(parsed))
}
pub(crate) async fn write_json<T>(
&self,
file: GovernanceStoreFile,
value: &T,
) -> anyhow::Result<()>
where
T: Serialize,
{
let payload = serde_json::to_string_pretty(value)?;
self.write_text(file, &payload).await
}
pub(crate) async fn write_text(
&self,
file: GovernanceStoreFile,
payload: &str,
) -> anyhow::Result<()> {
let path = self.path(file);
if Self::is_jsonl(file) {
anyhow::bail!(
"write_text is unsupported for JSONL governance store {}; use append_jsonl_line",
path.display()
);
}
crate::encrypted_file_store::write_text_file(path, payload)
.await
.with_context(|| format!("write governance store {}", path.display()))?;
Ok(())
}
pub(crate) async fn append_jsonl_line(
&self,
file: GovernanceStoreFile,
line: &str,
durable: bool,
) -> anyhow::Result<()> {
let path = self.path(file);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
let stored_line = crate::encrypted_file_store::encrypt_jsonl_line(line)?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
.with_context(|| format!("open governance JSONL store {}", path.display()))?;
file.write_all(stored_line.as_bytes()).await?;
file.write_all(b"\n").await?;
file.flush().await?;
if durable {
file.sync_all().await?;
}
Ok(())
}
pub(crate) async fn read_jsonl_lines(
&self,
file: GovernanceStoreFile,
) -> anyhow::Result<Option<Vec<String>>> {
let path = self.path(file);
let content = match fs::read_to_string(path).await {
Ok(content) => content,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err)
.with_context(|| format!("read governance JSONL store {}", path.display()))
}
};
let mut lines = Vec::new();
for line in content.lines() {
match crate::encrypted_file_store::decrypt_jsonl_line(line)
.with_context(|| format!("decrypt governance JSONL store {}", path.display()))?
{
Some(plaintext) => lines.push(plaintext),
None => continue,
}
}
Ok(Some(lines))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn file_store_json_and_jsonl_round_trip() {
let state = crate::test_support::test_state().await;
let store = for_state(&state);
let value = std::collections::HashMap::from([(
"decision-a".to_string(),
serde_json::json!({"result": "allow"}),
)]);
store
.write_json(GovernanceStoreFile::PolicyDecisions, &value)
.await
.expect("write json");
let loaded: std::collections::HashMap<String, serde_json::Value> = store
.read_json(GovernanceStoreFile::PolicyDecisions)
.await
.expect("read json")
.expect("json exists");
assert_eq!(loaded, value);
store
.append_jsonl_line(GovernanceStoreFile::ProtectedAudit, r#"{"seq":1}"#, true)
.await
.expect("append jsonl");
let lines = store
.read_jsonl_lines(GovernanceStoreFile::ProtectedAudit)
.await
.expect("read jsonl")
.expect("jsonl exists");
assert!(lines.iter().any(|line| line.contains(r#""seq":1"#)));
}
#[tokio::test]
async fn org_unit_registries_load_through_store() {
let state = crate::test_support::test_state().await;
let store = for_state(&state);
let fixture = tandem_enterprise_contract::authority::fixtures::acme_company();
let units = fixture
.graph
.units
.iter()
.map(|unit| {
(
format!("{}/{}", unit.taxonomy_id, unit.unit_id),
unit.clone(),
)
})
.collect::<std::collections::HashMap<_, _>>();
let memberships = fixture
.graph
.memberships
.iter()
.map(|membership| (membership.membership_id.clone(), membership.clone()))
.collect::<std::collections::HashMap<_, _>>();
let grants = fixture
.graph
.unit_access_grants
.iter()
.map(|grant| (grant.grant_id.clone(), grant.clone()))
.collect::<std::collections::HashMap<_, _>>();
store
.write_json(GovernanceStoreFile::OrgUnits, &units)
.await
.expect("write org units");
store
.write_json(GovernanceStoreFile::OrgUnitMemberships, &memberships)
.await
.expect("write memberships");
store
.write_json(GovernanceStoreFile::OrgUnitAccessGrants, &grants)
.await
.expect("write grants");
state.load_enterprise_org_units().await.expect("load units");
state
.load_enterprise_org_unit_memberships()
.await
.expect("load memberships");
state
.load_enterprise_org_unit_access_grants()
.await
.expect("load grants");
assert_eq!(state.enterprise.org_units.read().await.len(), units.len());
assert_eq!(
state.enterprise.org_unit_memberships.read().await.len(),
memberships.len()
);
assert_eq!(
state.enterprise.org_unit_access_grants.read().await.len(),
grants.len()
);
}
}