use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde::Serialize;
use crate::config::{Config, ConfigError};
use crate::name::{collab_cap, fold_fingerprint, layer_cap, Participant};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Client {
pub grant: String,
pub principal: String,
pub label: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Enrollment {
pub grants: BTreeMap<String, Vec<String>>,
pub clients: BTreeMap<String, Client>,
pub unenrolled: Vec<String>,
pub idle: Vec<String>,
}
pub fn load_fingerprints(
dir: &Path,
config: &Config,
) -> Result<BTreeMap<String, BTreeSet<String>>, ConfigError> {
let path = dir.join("enrollment.toml");
let at = path.display();
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeMap::new()),
Err(e) => return Err(ConfigError(format!("read {at}: {e}"))),
};
let table: toml::Table =
toml::from_str(&text).map_err(|e| ConfigError(format!("{at}: {e}")))?;
let mut fingerprints: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (key, value) in table {
let participant =
Participant::from_manifest_key(&key).map_err(|e| ConfigError(format!("{at}: {e}")))?;
if !config.participants.names(participant.iri()) {
return Err(ConfigError(format!(
"{at}: `{key}` is no participant in participants.ttl"
)));
}
let raw: Vec<String> = match value {
toml::Value::String(s) => vec![s],
toml::Value::Array(items) => items
.into_iter()
.map(|i| match i {
toml::Value::String(s) => Ok(s),
_ => Err(ConfigError(format!("{at}: `{key}` lists a non-string"))),
})
.collect::<Result<_, _>>()?,
_ => {
return Err(ConfigError(format!(
"{at}: `{key}` is a fingerprint or a list of them"
)))
}
};
let set = fingerprints
.entry(participant.iri().to_string())
.or_default();
for fp in raw {
set.insert(fold_fingerprint(&fp).ok_or_else(|| {
ConfigError(format!(
"{at}: `{key}`: `{fp}` is not a SHA-256 fingerprint (64 hex digits)"
))
})?);
}
}
Ok(fingerprints)
}
pub fn generate(
config: &Config,
fingerprints: &BTreeMap<String, BTreeSet<String>>,
) -> Result<Enrollment, ConfigError> {
let mut scopes: BTreeMap<Participant, BTreeSet<String>> = BTreeMap::new();
for manifest in config.collabs.values() {
for mount in &manifest.mounts {
for (participant, verbs) in &mount.grants {
let held = scopes.entry(participant.clone()).or_default();
held.insert(collab_cap(&manifest.name));
held.extend(verbs.iter().map(|verb| layer_cap(verb, &mount.layer)));
}
}
}
let mut enrollment = Enrollment::default();
let mut owner: BTreeMap<String, String> = BTreeMap::new();
for (participant, held) in &scopes {
enrollment.grants.insert(
participant.slug().to_string(),
held.iter().cloned().collect(),
);
let mut certs: BTreeSet<String> = fingerprints
.get(participant.iri())
.cloned()
.unwrap_or_default();
certs.extend(participant.fingerprint().map(str::to_string));
if certs.is_empty() {
enrollment.unenrolled.push(participant.iri().to_string());
}
let label = config.participants.members[participant.iri()]
.label
.clone()
.unwrap_or_else(|| participant.slug().to_string());
for fp in certs {
if let Some(other) = owner.insert(fp.clone(), participant.iri().to_string()) {
return Err(ConfigError(format!(
"certificate {fp} is enrolled for both {other} and {}: one certificate \
names one actor",
participant.iri()
)));
}
enrollment.clients.insert(
fp,
Client {
grant: participant.slug().to_string(),
principal: participant.iri().to_string(),
label: label.clone(),
},
);
}
}
for (iri, certs) in fingerprints {
if scopes.keys().any(|p| p.iri() == iri) {
continue;
}
for fp in certs {
if let Some(other) = owner.insert(fp.clone(), iri.clone()) {
return Err(ConfigError(format!(
"certificate {fp} is enrolled for both {other} and {iri}"
)));
}
}
enrollment.idle.push(iri.clone());
}
Ok(enrollment)
}
impl Enrollment {
pub fn grants_json(&self) -> String {
let mut text = serde_json::to_string_pretty(&self.grants).expect("a map of strings");
text.push('\n');
text
}
pub fn clients_json(&self) -> String {
let mut text =
serde_json::to_string_pretty(&serde_json::json!({ "clients": self.clients }))
.expect("a map of strings");
text.push('\n');
text
}
pub fn write(&self, dir: &Path) -> Result<(), String> {
for (name, text) in [
("grants.json", self.grants_json()),
("clients.json", self.clients_json()),
] {
let path = dir.join(name);
let tmp = dir.join(format!(".{name}.tmp"));
std::fs::write(&tmp, text)
.and_then(|()| std::fs::rename(&tmp, &path))
.map_err(|e| format!("write {}: {e}", path.display()))?;
}
Ok(())
}
}