1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! Operator bounds for the single grant-log reader and admission verifier.
use serde::Deserialize;
use std::path::PathBuf;
/// One identified log, read with the app's credential, never a person's session.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GrantStreamConfig {
/// Stable log identity; comparison prevents crossing a reset domain.
pub log_identity: String,
/// Reset epoch, compared together with the identity.
pub log_epoch: u64,
/// App credential, read once and zeroized; its bytes are never diagnostic data.
pub credential_path: PathBuf,
/// Public grant-event service key as 64 lowercase hex digits, pinned locally.
pub service_key_path: PathBuf,
/// Log positions per read; limits replay work independently of history length.
pub page_positions: u64,
/// Frames per answer; bounds placement and readiness overhead as well as changes.
pub max_frames: usize,
/// Bytes per HTTP answer before JSON decoding; prevents an unbounded allocation.
pub max_response_bytes: usize,
/// Bytes per signed event before hex decoding and signature verification.
pub max_event_bytes: usize,
/// Revoked ids retained from the complete baseline; never evicted into an allow.
pub max_revoked_grants: usize,
/// Credential bytes, checked before reading a file or verifying a pass.
pub max_credential_bytes: usize,
/// Published key-set bytes, checked before loading the verification cache.
pub max_keyset_bytes: usize,
/// Transport-ended reopens without cursor progress; bounds a failing peer's work.
pub max_transport_reopens: usize,
/// Rights in a bound pass; caps local admission work and the dependency request.
pub max_rights: usize,
/// Ancestors per right, including its leaf; prevents over-bound verification.
pub max_chain_grants: usize,
/// Independent paths per operation; preserves every accepted path without truncation.
pub max_chains: usize,
/// Enforce the signed binding and reason-bearing close on every connection.
pub enforce_revocation: bool,
/// Exact signed target whose outright right admits a connection under that policy.
pub admission: lys_pass::Target,
/// Exact placed resources mapped to subject prefixes, never inferred from ids.
pub subjects: Vec<GrantSubjectConfig>,
}
/// Mapping only: the verified pass still decides each actual subject and action.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GrantSubjectConfig {
/// Qualified resource kind in the signed rights.
pub kind: String,
/// Exact placed resource id in the signed rights.
pub resource: String,
/// Subject prefix whose hot operations depend on those rights.
pub prefix: String,
}
impl GrantStreamConfig {
/// Validates bounds before allocating, reading credentials or starting a reader.
///
/// # Errors
/// Names every invalid operator field; no input value or credential is printed.
pub fn validate(&self) -> Result<(), crate::ServerError> {
let mut errors = Vec::new();
self.collect_errors(&mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(crate::ServerError::ConfigValidation {
message: errors.join("\n"),
})
}
}
pub(crate) fn collect_errors(&self, errors: &mut Vec<String>) {
use crate::auth_revocation as rev;
let prefix = "auth.lys.grant_stream";
for (field, value) in [
("max_frames", self.max_frames),
("max_response_bytes", self.max_response_bytes),
("max_event_bytes", self.max_event_bytes),
("max_revoked_grants", self.max_revoked_grants),
("max_credential_bytes", self.max_credential_bytes),
("max_keyset_bytes", self.max_keyset_bytes),
("max_transport_reopens", self.max_transport_reopens),
("max_rights", self.max_rights),
("max_chain_grants", self.max_chain_grants),
("max_chains", self.max_chains),
] {
if value == 0 {
errors.push(format!("{prefix}.{field}: must be positive"));
}
}
if self.page_positions == 0 {
errors.push(format!("{prefix}.page_positions: must be positive"));
}
if usize::try_from(self.page_positions)
.ok()
.and_then(|positions| positions.checked_add(2))
.is_none_or(|frames| frames > self.max_frames)
{
errors.push(format!(
"{prefix}.max_frames: must cover page_positions and both barriers"
));
}
if self.max_event_bytes > self.max_response_bytes / 2 {
errors.push(format!(
"{prefix}.max_event_bytes: hex event must fit max_response_bytes"
));
}
for (field, value, ceiling) in [
("max_rights", self.max_rights, rev::MAX_BOUND_RIGHTS),
(
"max_chain_grants",
self.max_chain_grants,
rev::MAX_CHAIN_LEN,
),
("max_chains", self.max_chains, rev::MAX_CHAINS_PER_RIGHT),
(
"max_revoked_grants",
self.max_revoked_grants,
rev::MAX_DECISIONS_PER_EVENT,
),
] {
if value > ceiling {
errors.push(format!(
"{prefix}.{field}: exceeds authority contract {ceiling}"
));
}
}
if self.log_identity.is_empty()
|| self.log_identity.len() > rev::MAX_LOG_IDENTITY_LEN
|| self.log_identity.chars().any(char::is_control)
{
errors.push(format!("{prefix}.log_identity: invalid identified log"));
}
for (field, path) in [
("credential_path", &self.credential_path),
("service_key_path", &self.service_key_path),
] {
if path.as_os_str().is_empty() {
errors.push(format!("{prefix}.{field}: must name a file"));
}
}
if lys_pass::Target::new(
&self.admission.kind,
&self.admission.id,
&self.admission.action,
)
.is_err()
{
errors.push(format!("{prefix}.admission: an exact target is required"));
}
let mut seen = std::collections::BTreeSet::new();
for mapping in &self.subjects {
if [&mapping.kind, &mapping.resource, &mapping.prefix]
.iter()
.any(|value| value.is_empty() || value.chars().any(char::is_control))
|| mapping.prefix.len() > rev::MAX_RIGHT_PREFIX_LEN
|| mapping.prefix.contains(['*', '>'])
{
errors.push(format!("{prefix}.subjects: invalid exact resource mapping"));
}
if !seen.insert((&mapping.kind, &mapping.resource, &mapping.prefix)) {
errors.push(format!("{prefix}.subjects: duplicate mapping"));
}
}
if self.subjects.len() > self.max_rights {
errors.push(format!("{prefix}.subjects: exceeds max_rights"));
}
}
}