1use std::collections::BTreeMap;
16
17use rustfs_extension_schema::{
18 ExtensionContractError, ExtensionKind, ExtensionSchema, OPS_DIAGNOSTICS_CAPABILITY, OpsDiagnosticSurface,
19 OpsDiagnosticsContract, validate_ops_diagnostics_contract,
20};
21use thiserror::Error;
22
23#[derive(Debug, Error, PartialEq, Eq)]
24pub enum OpsDiagnosticsRegistryError {
25 #[error(transparent)]
26 InvalidContract(#[from] ExtensionContractError),
27
28 #[error("extension {extension_id} is {kind:?}, not an ops diagnostics extension")]
29 UnsupportedExtensionKind { extension_id: String, kind: ExtensionKind },
30
31 #[error("ops diagnostics extension {extension_id} is missing capability {capability}")]
32 MissingCapability { extension_id: String, capability: &'static str },
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct OpsDiagnosticsRegistration {
37 pub extension_id: String,
38 pub surface: OpsDiagnosticSurface,
39}
40
41#[derive(Debug, Default, Clone, PartialEq, Eq)]
42pub struct OpsDiagnosticsRegistry {
43 registrations: BTreeMap<OpsDiagnosticSurface, Vec<OpsDiagnosticsRegistration>>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct OpsDiagnosticsReadRequest<'a> {
48 pub surface: OpsDiagnosticSurface,
49 pub capability: &'a str,
50 pub admin_action_authorized: bool,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum OpsDiagnosticsAccessDecision {
55 AllowReadOnly,
56 DenyMissingAdminAction,
57 DenyMissingCapability,
58 DenyUnknownSurface,
59}
60
61impl OpsDiagnosticsRegistry {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn register_schema(
67 &mut self,
68 schema: &ExtensionSchema,
69 contract: &OpsDiagnosticsContract,
70 ) -> Result<(), OpsDiagnosticsRegistryError> {
71 if schema.kind != ExtensionKind::OpsDiagnostics {
72 return Err(OpsDiagnosticsRegistryError::UnsupportedExtensionKind {
73 extension_id: schema.extension_id.clone(),
74 kind: schema.kind,
75 });
76 }
77
78 if !schema
79 .capabilities
80 .iter()
81 .any(|capability| capability.as_str() == OPS_DIAGNOSTICS_CAPABILITY)
82 {
83 return Err(OpsDiagnosticsRegistryError::MissingCapability {
84 extension_id: schema.extension_id.clone(),
85 capability: OPS_DIAGNOSTICS_CAPABILITY,
86 });
87 }
88
89 validate_ops_diagnostics_contract(contract)?;
90
91 for surface in &contract.surfaces {
92 self.registrations
93 .entry(*surface)
94 .or_default()
95 .push(OpsDiagnosticsRegistration {
96 extension_id: schema.extension_id.clone(),
97 surface: *surface,
98 });
99 }
100
101 Ok(())
102 }
103
104 pub fn registered_surface_count(&self) -> usize {
105 self.registrations.values().map(Vec::len).sum()
106 }
107
108 pub fn registrations_for(&self, surface: OpsDiagnosticSurface) -> impl Iterator<Item = &OpsDiagnosticsRegistration> {
109 self.registrations.get(&surface).into_iter().flatten()
110 }
111
112 pub fn authorize_read(&self, request: OpsDiagnosticsReadRequest<'_>) -> OpsDiagnosticsAccessDecision {
113 if !request.admin_action_authorized {
116 return OpsDiagnosticsAccessDecision::DenyMissingAdminAction;
117 }
118
119 if !self.registrations.contains_key(&request.surface) {
120 return OpsDiagnosticsAccessDecision::DenyUnknownSurface;
121 }
122
123 if request.capability != OPS_DIAGNOSTICS_CAPABILITY {
124 return OpsDiagnosticsAccessDecision::DenyMissingCapability;
125 }
126
127 OpsDiagnosticsAccessDecision::AllowReadOnly
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::{OpsDiagnosticsAccessDecision, OpsDiagnosticsReadRequest, OpsDiagnosticsRegistry, OpsDiagnosticsRegistryError};
134 use crate::{builtin_ops_diagnostics_contract, builtin_ops_diagnostics_extension_schema};
135 use rustfs_extension_schema::{ExtensionContractError, ExtensionKind, OPS_DIAGNOSTICS_CAPABILITY, OpsDiagnosticSurface};
136
137 #[test]
138 fn default_registry_denies_unknown_diagnostic_surface() {
139 let registry = OpsDiagnosticsRegistry::new();
140
141 assert_eq!(
142 registry.authorize_read(OpsDiagnosticsReadRequest {
143 surface: OpsDiagnosticSurface::Health,
144 capability: OPS_DIAGNOSTICS_CAPABILITY,
145 admin_action_authorized: true,
146 }),
147 OpsDiagnosticsAccessDecision::DenyUnknownSurface
148 );
149 }
150
151 #[test]
152 fn registered_ops_diagnostics_are_read_only_and_capability_limited() {
153 let mut registry = OpsDiagnosticsRegistry::new();
154 let schema = builtin_ops_diagnostics_extension_schema();
155 let contract = builtin_ops_diagnostics_contract();
156
157 registry
158 .register_schema(&schema, &contract)
159 .expect("builtin ops diagnostics contract should register");
160
161 assert_eq!(registry.registered_surface_count(), contract.surfaces.len());
162 assert_eq!(registry.registrations_for(OpsDiagnosticSurface::Metrics).count(), 1);
163 assert_eq!(
164 registry.authorize_read(OpsDiagnosticsReadRequest {
165 surface: OpsDiagnosticSurface::Health,
166 capability: OPS_DIAGNOSTICS_CAPABILITY,
167 admin_action_authorized: true,
168 }),
169 OpsDiagnosticsAccessDecision::AllowReadOnly
170 );
171 assert_eq!(
172 registry.authorize_read(OpsDiagnosticsReadRequest {
173 surface: OpsDiagnosticSurface::Health,
174 capability: "target.notify.v1",
175 admin_action_authorized: true,
176 }),
177 OpsDiagnosticsAccessDecision::DenyMissingCapability
178 );
179 assert_eq!(
180 registry.authorize_read(OpsDiagnosticsReadRequest {
181 surface: OpsDiagnosticSurface::Health,
182 capability: OPS_DIAGNOSTICS_CAPABILITY,
183 admin_action_authorized: false,
184 }),
185 OpsDiagnosticsAccessDecision::DenyMissingAdminAction
186 );
187 }
188
189 #[test]
190 fn unauthorized_read_does_not_leak_surface_existence() {
191 let mut registry = OpsDiagnosticsRegistry::new();
192 let mut contract = builtin_ops_diagnostics_contract();
194 contract.surfaces = vec![OpsDiagnosticSurface::Metrics];
195 registry
196 .register_schema(&builtin_ops_diagnostics_extension_schema(), &contract)
197 .expect("subset ops diagnostics contract should register");
198
199 let registered = registry.authorize_read(OpsDiagnosticsReadRequest {
202 surface: OpsDiagnosticSurface::Metrics,
203 capability: OPS_DIAGNOSTICS_CAPABILITY,
204 admin_action_authorized: false,
205 });
206 let unknown = registry.authorize_read(OpsDiagnosticsReadRequest {
207 surface: OpsDiagnosticSurface::Health,
208 capability: OPS_DIAGNOSTICS_CAPABILITY,
209 admin_action_authorized: false,
210 });
211
212 assert_eq!(registered, OpsDiagnosticsAccessDecision::DenyMissingAdminAction);
213 assert_eq!(unknown, OpsDiagnosticsAccessDecision::DenyMissingAdminAction);
214 }
215
216 #[test]
217 fn rejects_non_diagnostics_schema_and_mutating_contracts() {
218 let mut registry = OpsDiagnosticsRegistry::new();
219 let mut schema = builtin_ops_diagnostics_extension_schema();
220 schema.kind = ExtensionKind::TargetPlugin;
221
222 assert_eq!(
223 registry
224 .register_schema(&schema, &builtin_ops_diagnostics_contract())
225 .expect_err("target plugin schema should not register as ops diagnostics"),
226 OpsDiagnosticsRegistryError::UnsupportedExtensionKind {
227 extension_id: "builtin:ops-diagnostics".to_string(),
228 kind: ExtensionKind::TargetPlugin
229 }
230 );
231
232 schema.kind = ExtensionKind::OpsDiagnostics;
233 let mut contract = builtin_ops_diagnostics_contract();
234 contract.mutates_object_data = true;
235
236 assert_eq!(
237 registry
238 .register_schema(&schema, &contract)
239 .expect_err("object mutation should be rejected"),
240 OpsDiagnosticsRegistryError::InvalidContract(ExtensionContractError::OpsDiagnosticsMutatesObjectData)
241 );
242 }
243}