Skip to main content

rustfs_targets/runtime/
ops_profiler.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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        // Authorize before probing the registry: checking existence first would
127        // let an unauthorized caller distinguish a registered backend
128        // (DenyMissingAdminAction) from an unknown one (DenyUnknownBackend),
129        // leaking registry contents.
130        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        // A registered backend and an unknown backend must be indistinguishable
221        // to an unauthorized caller: both deny on authorization, not existence.
222        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}