1use std::collections::BTreeMap;
16
17use rustfs_extension_schema::{
18 ExtensionCapabilityRef, ExtensionContractError, ExtensionKind, ExtensionSchema, OPS_PROFILER_CAPABILITY,
19 OpsProfilerBackendStatus, OpsProfilerCapabilitySnapshot, OpsProfilerContract, OpsProfilerRuntimeSnapshot,
20 validate_ops_profiler_capability_snapshot,
21};
22use thiserror::Error;
23
24#[derive(Debug, Error, PartialEq, Eq)]
25pub enum OpsProfilerRegistryError {
26 #[error(transparent)]
27 InvalidContract(#[from] ExtensionContractError),
28
29 #[error("extension {extension_id} is {kind:?}, not an ops profiler extension")]
30 UnsupportedExtensionKind { extension_id: String, kind: ExtensionKind },
31
32 #[error("ops profiler extension {extension_id} is missing capability {capability}")]
33 MissingCapability { extension_id: String, capability: &'static str },
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct OpsProfilerRegistration {
38 pub extension_id: String,
39 pub backend: String,
40 pub status: OpsProfilerBackendStatus,
41 pub supports_profile_export: bool,
42}
43
44#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct OpsProfilerRegistry {
46 registrations: BTreeMap<String, OpsProfilerRegistration>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct OpsProfilerReadRequest<'a> {
51 pub backend: &'a str,
52 pub capability: &'a str,
53 pub admin_action_authorized: bool,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum OpsProfilerAccessDecision {
58 AllowReadOnly,
59 DenyMissingAdminAction,
60 DenyMissingCapability,
61 DenyUnknownBackend,
62}
63
64impl OpsProfilerRegistry {
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn register_schema(
70 &mut self,
71 schema: &ExtensionSchema,
72 contract: &OpsProfilerContract,
73 ) -> Result<(), OpsProfilerRegistryError> {
74 if schema.kind != ExtensionKind::OpsProfiler {
75 return Err(OpsProfilerRegistryError::UnsupportedExtensionKind {
76 extension_id: schema.extension_id.clone(),
77 kind: schema.kind,
78 });
79 }
80
81 if !schema
82 .capabilities
83 .iter()
84 .any(|capability| capability.as_str() == OPS_PROFILER_CAPABILITY)
85 {
86 return Err(OpsProfilerRegistryError::MissingCapability {
87 extension_id: schema.extension_id.clone(),
88 capability: OPS_PROFILER_CAPABILITY,
89 });
90 }
91
92 validate_ops_profiler_capability_snapshot(&OpsProfilerCapabilitySnapshot {
93 capability: ExtensionCapabilityRef::new(OPS_PROFILER_CAPABILITY),
94 runtime: OpsProfilerRuntimeSnapshot {
95 boundary: schema.runtime.boundary,
96 disabled_by_default: schema.disabled_by_default,
97 startup_fatal: false,
98 },
99 contract: contract.clone(),
100 })?;
101
102 for backend in &contract.backends {
103 self.registrations.insert(
104 backend.backend.as_str().to_string(),
105 OpsProfilerRegistration {
106 extension_id: schema.extension_id.clone(),
107 backend: backend.backend.as_str().to_string(),
108 status: backend.status,
109 supports_profile_export: backend.supports_profile_export,
110 },
111 );
112 }
113
114 Ok(())
115 }
116
117 pub fn registered_backend_count(&self) -> usize {
118 self.registrations.len()
119 }
120
121 pub fn registration_for(&self, backend: &str) -> Option<&OpsProfilerRegistration> {
122 self.registrations.get(backend)
123 }
124
125 pub fn authorize_read(&self, request: OpsProfilerReadRequest<'_>) -> OpsProfilerAccessDecision {
126 if !request.admin_action_authorized {
131 return OpsProfilerAccessDecision::DenyMissingAdminAction;
132 }
133
134 if !self.registrations.contains_key(request.backend) {
135 return OpsProfilerAccessDecision::DenyUnknownBackend;
136 }
137
138 if request.capability != OPS_PROFILER_CAPABILITY {
139 return OpsProfilerAccessDecision::DenyMissingCapability;
140 }
141
142 OpsProfilerAccessDecision::AllowReadOnly
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::{OpsProfilerAccessDecision, OpsProfilerReadRequest, OpsProfilerRegistry, OpsProfilerRegistryError};
149 use crate::{builtin_ops_profiler_contract, builtin_ops_profiler_extension_schema};
150 use rustfs_extension_schema::{ExtensionContractError, ExtensionKind, OPS_PROFILER_CAPABILITY, OpsProfilerBackendStatus};
151
152 #[test]
153 fn default_registry_denies_unknown_profiler_backend() {
154 let registry = OpsProfilerRegistry::new();
155
156 assert_eq!(
157 registry.authorize_read(OpsProfilerReadRequest {
158 backend: "cpu_pprof",
159 capability: OPS_PROFILER_CAPABILITY,
160 admin_action_authorized: true,
161 }),
162 OpsProfilerAccessDecision::DenyUnknownBackend
163 );
164 }
165
166 #[test]
167 fn registered_ops_profiler_backends_are_read_only_and_capability_limited() {
168 let mut registry = OpsProfilerRegistry::new();
169 let schema = builtin_ops_profiler_extension_schema();
170 let contract = builtin_ops_profiler_contract();
171
172 registry
173 .register_schema(&schema, &contract)
174 .expect("builtin ops profiler contract should register");
175
176 assert_eq!(registry.registered_backend_count(), contract.backends.len());
177 assert_eq!(
178 registry.registration_for("cpu_pprof").map(|registration| registration.status),
179 Some(OpsProfilerBackendStatus::Unsupported)
180 );
181 assert_eq!(
182 registry
183 .registration_for("memory_pprof")
184 .map(|registration| registration.supports_profile_export),
185 Some(false)
186 );
187 assert_eq!(
188 registry.authorize_read(OpsProfilerReadRequest {
189 backend: "cpu_pprof",
190 capability: OPS_PROFILER_CAPABILITY,
191 admin_action_authorized: true,
192 }),
193 OpsProfilerAccessDecision::AllowReadOnly
194 );
195 assert_eq!(
196 registry.authorize_read(OpsProfilerReadRequest {
197 backend: "cpu_pprof",
198 capability: "ops.diagnostics.v1",
199 admin_action_authorized: true,
200 }),
201 OpsProfilerAccessDecision::DenyMissingCapability
202 );
203 assert_eq!(
204 registry.authorize_read(OpsProfilerReadRequest {
205 backend: "cpu_pprof",
206 capability: OPS_PROFILER_CAPABILITY,
207 admin_action_authorized: false,
208 }),
209 OpsProfilerAccessDecision::DenyMissingAdminAction
210 );
211 }
212
213 #[test]
214 fn unauthorized_read_does_not_leak_backend_existence() {
215 let mut registry = OpsProfilerRegistry::new();
216 registry
217 .register_schema(&builtin_ops_profiler_extension_schema(), &builtin_ops_profiler_contract())
218 .expect("builtin ops profiler contract should register");
219
220 let registered = registry.authorize_read(OpsProfilerReadRequest {
223 backend: "cpu_pprof",
224 capability: OPS_PROFILER_CAPABILITY,
225 admin_action_authorized: false,
226 });
227 let unknown = registry.authorize_read(OpsProfilerReadRequest {
228 backend: "does_not_exist",
229 capability: OPS_PROFILER_CAPABILITY,
230 admin_action_authorized: false,
231 });
232
233 assert_eq!(registered, OpsProfilerAccessDecision::DenyMissingAdminAction);
234 assert_eq!(unknown, OpsProfilerAccessDecision::DenyMissingAdminAction);
235 }
236
237 #[test]
238 fn rejects_non_profiler_schema_and_invalid_runtime_boundaries() {
239 let mut registry = OpsProfilerRegistry::new();
240 let mut schema = builtin_ops_profiler_extension_schema();
241 schema.kind = ExtensionKind::TargetPlugin;
242
243 assert_eq!(
244 registry
245 .register_schema(&schema, &builtin_ops_profiler_contract())
246 .expect_err("target plugin schema should not register as ops profiler"),
247 OpsProfilerRegistryError::UnsupportedExtensionKind {
248 extension_id: "builtin:ops-profiler".to_string(),
249 kind: ExtensionKind::TargetPlugin
250 }
251 );
252
253 schema.kind = ExtensionKind::OpsProfiler;
254 schema.runtime.boundary = rustfs_extension_schema::ExtensionRuntimeBoundary::Sidecar;
255 schema.disabled_by_default = false;
256
257 assert_eq!(
258 registry
259 .register_schema(&schema, &builtin_ops_profiler_contract())
260 .expect_err("external profiler runtimes must be disabled by default"),
261 OpsProfilerRegistryError::InvalidContract(ExtensionContractError::OpsProfilerExternalRuntimeEnabledByDefault)
262 );
263 }
264}