use std::collections::BTreeSet;
use std::os::unix::fs::PermissionsExt;
use super::types::{ServerConfig, ServiceTemplateConfig};
pub(super) fn validate_templates(config: &ServerConfig, errors: &mut Vec<String>) {
let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
return;
};
if lys.templates.is_empty() {
return;
}
if config.persistence_path.is_none() {
errors.push(
"auth.lys.templates: a service registry needs persistence_path to be kept".to_owned(),
);
}
if lys.app_credential_path.is_none() {
errors.push(
"auth.lys.templates: app_credential_path is required to place a new service in Lys"
.to_owned(),
);
}
let mut names = BTreeSet::new();
let mut roots = BTreeSet::new();
for template in &lys.templates {
if !names.insert(template.name.as_str()) {
errors.push(format!(
"auth.lys.templates '{}': the name is used twice",
template.name
));
}
for root in &template.subjects {
if !roots.insert(root.as_str()) {
errors.push(format!(
"auth.lys.templates '{}': subject '{root}' is another template's too",
template.name
));
}
}
errors.extend(
template_faults(template)
.into_iter()
.map(|fault| format!("auth.lys.templates '{}': {fault}", template.name)),
);
}
}
fn template_faults(template: &ServiceTemplateConfig) -> Vec<String> {
let mut faults = Vec::new();
if !is_segment(&template.name) {
faults.push("the name must be one segment of letters, digits, '-' or '_'".to_owned());
}
if template.subjects.is_empty() {
faults.push("subjects must name at least one subject".to_owned());
}
if let Some(bad) = template.subjects.iter().find(|root| !is_subject(root)) {
faults.push(format!(
"subject '{bad}' must be dot-separated segments with no wildcard"
));
}
if template.stop_timeout_ms == 0 {
faults.push("stop_timeout_ms must be positive".to_owned());
}
if template.command.is_absolute() {
match std::fs::metadata(&template.command) {
Ok(meta) if meta.is_file() && meta.permissions().mode() & 0o111 != 0 => {}
Ok(_) => faults.push(format!(
"command '{}' is not an executable file",
template.command.display()
)),
Err(error) => faults.push(format!(
"command '{}' cannot be read: {}",
template.command.display(),
error.kind()
)),
}
} else {
faults.push(format!(
"command '{}' must be an absolute path",
template.command.display()
));
}
faults
}
#[must_use]
pub fn is_segment(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
fn is_subject(value: &str) -> bool {
value.split('.').all(is_segment)
}
#[cfg(test)]
#[path = "templates_tests.rs"]
mod tests;