khive_runtime/credentials/
config.rs1use std::collections::BTreeSet;
2
3use serde::{de::DeserializeOwned, Deserialize};
4
5use super::CredentialError;
6use crate::engine_config::KhiveConfig;
7
8pub(crate) fn read_tables(raw: &str, config: &mut KhiveConfig) -> Result<(), CredentialError> {
12 let mut document: toml::Table = toml::from_str(raw).map_err(|_| refused("config".into()))?;
13 if let Some(value) = document.remove("credentials") {
14 let toml::Value::Array(entries) = value else {
15 return Err(refused("credentials".into()));
16 };
17 config.credentials = entries
18 .into_iter()
19 .enumerate()
20 .map(|(index, entry)| closed(entry, || format!("credentials[{index}]")))
21 .collect::<Result<_, _>>()?;
22 }
23 if let Some(value) = document.remove("visibility_receipts") {
24 config.visibility_receipts = Some(closed(value, || "visibility_receipts".into())?);
25 }
26 Ok(())
27}
28
29fn closed<T: DeserializeOwned>(
30 value: toml::Value,
31 name: impl FnOnce() -> String,
32) -> Result<T, CredentialError> {
33 value.try_into().map_err(|_| refused(name()))
34}
35
36fn refused(name: String) -> CredentialError {
37 CredentialError::InvalidConfig {
38 name,
39 reason: "unknown, missing or invalid field; values are never shown",
40 }
41}
42
43#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum CredentialKind {
46 Header,
47 Basic,
48 CookieJar,
49 SigningKey,
50}
51
52#[derive(Debug, Clone, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct CredentialConfig {
56 pub name: String,
57 pub kind: CredentialKind,
58 pub provider: String,
59 pub env_var: Option<String>,
60 pub header: Option<String>,
61}
62
63impl CredentialConfig {
64 pub fn validate_all(entries: &[Self]) -> Result<(), CredentialError> {
65 let mut names = BTreeSet::new();
66 for entry in entries {
67 let invalid = |reason| CredentialError::InvalidConfig {
68 name: entry.name.clone(),
69 reason,
70 };
71 if entry.name.is_empty() || !names.insert(&entry.name) {
72 return Err(invalid("credential names must be nonempty and unique"));
73 }
74 if entry.provider.is_empty() {
75 return Err(invalid("provider must be nonempty"));
76 }
77 if entry.provider == "env" {
78 if entry
79 .env_var
80 .as_deref()
81 .is_none_or(|name| name.is_empty() || name.contains(['=', '\0']))
82 {
83 return Err(invalid("env provider requires a valid env_var name"));
84 }
85 } else if entry.env_var.is_some() {
86 return Err(invalid("env_var is only supported by the env provider"));
87 }
88 match (entry.kind, entry.header.as_deref()) {
89 (CredentialKind::Header, Some(header)) if valid_header_name(header) => {}
90 (CredentialKind::Header, _) => {
91 return Err(invalid("header credentials require a valid header name"));
92 }
93 (_, Some(_)) => return Err(invalid("header is only valid for header credentials")),
94 (_, None) => {}
95 }
96 }
97 Ok(())
98 }
99}
100
101fn valid_header_name(name: &str) -> bool {
102 !name.is_empty()
103 && name
104 .bytes()
105 .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte))
106}
107
108#[derive(Debug, Clone, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct VisibilityReceiptConfig {
112 #[serde(default)]
113 pub keys: Vec<VisibilityReceiptKeyConfig>,
114}
115
116#[derive(Debug, Clone, Deserialize)]
117#[serde(deny_unknown_fields)]
118pub struct VisibilityReceiptKeyConfig {
119 pub id: String,
120 pub credential: String,
121 #[serde(default)]
122 pub encrypt: bool,
123}
124
125impl VisibilityReceiptConfig {
126 pub fn validate(&self, credentials: &[CredentialConfig]) -> Result<(), CredentialError> {
127 CredentialConfig::validate_all(credentials)?;
128 let mut ids = BTreeSet::new();
129 let mut encrypting = 0;
130 for key in &self.keys {
131 let invalid = |reason| CredentialError::InvalidConfig {
132 name: "visibility_receipts".to_owned(),
133 reason,
134 };
135 if !(1..=64).contains(&key.id.len())
136 || !key
137 .id
138 .bytes()
139 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
140 {
141 return Err(invalid(
142 "key IDs require 1..64 ASCII letters, digits, '.', '_' or '-'",
143 ));
144 }
145 if !ids.insert(&key.id) {
146 return Err(invalid("key IDs must be unique"));
147 }
148 let Some(credential) = credentials
149 .iter()
150 .find(|entry| entry.name == key.credential)
151 else {
152 return Err(CredentialError::UnknownCredential {
153 name: key.credential.clone(),
154 });
155 };
156 if credential.kind != CredentialKind::SigningKey {
157 return Err(invalid(
158 "receipt keys must reference signing_key credentials",
159 ));
160 }
161 encrypting += usize::from(key.encrypt);
162 }
163 if encrypting != 1 {
164 return Err(CredentialError::InvalidConfig {
165 name: "visibility_receipts".to_owned(),
166 reason: "exactly one key must have encrypt = true",
167 });
168 }
169 Ok(())
170 }
171}