1use std::sync::{Arc, Mutex, OnceLock};
22use std::time::{Duration, Instant};
23
24use lean_ctx_fs::managed::{Carrier, Owner};
25use lean_ctx_protocol::hive::managed::{ManagedCarrierKindV1, ManagedConfigV1};
26use lean_ctx_protocol::hive::status::{HiveStatusManagedMemberV1, HiveStatusManagedV1};
27
28use super::hive::HiveConfig;
29
30const REREAD_AFTER: Duration = Duration::from_secs(5);
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Managed {
37 pub carrier: ManagedCarrierKindV1,
38 pub location: String,
40 pub config: Result<ManagedConfigV1, String>,
42}
43
44impl Managed {
45 pub fn load(carrier: &Carrier, owner: Owner) -> Option<Self> {
47 let kind = match carrier {
48 Carrier::MacosProfile(_) => ManagedCarrierKindV1::MacosProfile,
49 Carrier::WindowsPolicy(..) => ManagedCarrierKindV1::WindowsPolicy,
50 Carrier::File(_) => ManagedCarrierKindV1::LinuxFile,
51 };
52 let config = match lean_ctx_fs::managed::read(carrier, owner) {
53 Ok(None) => return None,
54 Ok(Some(members)) => {
55 ManagedConfigV1::from_members(&members).map_err(|error| error.to_string())
56 }
57 Err(error) => Err(error),
58 };
59 if config.as_ref().is_ok_and(ManagedConfigV1::is_empty) {
60 return None;
61 }
62 Some(Self {
63 carrier: kind,
64 location: carrier.location(),
65 config,
66 })
67 }
68
69 pub fn manager(&self) -> String {
71 match self
72 .config
73 .as_ref()
74 .ok()
75 .and_then(|c| c.hive_org.as_deref())
76 {
77 Some(org) => format!("this machine is managed by {org}"),
78 None => "this machine is managed by your organisation".to_string(),
79 }
80 }
81
82 pub fn apply_to_hive(&self, hive: &mut HiveConfig) {
86 let Ok(config) = &self.config else {
87 return;
88 };
89 if let Some(url) = &config.hive_url {
90 hive.url = Some(url.clone());
91 }
92 if let Some(key) = &config.hive_root_key {
93 hive.root_key = Some(key.clone());
94 }
95 if config.telemetry_metrics_upload == Some(false) {
96 hive.metrics_upload = false;
97 }
98 if config.telemetry_receipts_upload == Some(false) {
99 hive.receipts_upload = false;
100 }
101 }
102
103 pub fn keep_user_values(&self, hive: &mut HiveConfig, user: &HiveConfig) {
106 let Ok(config) = &self.config else {
107 return;
108 };
109 if config.hive_url.is_some() {
110 hive.url.clone_from(&user.url);
111 }
112 if config.hive_root_key.is_some() {
113 hive.root_key.clone_from(&user.root_key);
114 }
115 if config.telemetry_metrics_upload == Some(false) {
116 hive.metrics_upload = user.metrics_upload;
117 }
118 if config.telemetry_receipts_upload == Some(false) {
119 hive.receipts_upload = user.receipts_upload;
120 }
121 }
122
123 pub fn refusal(&self, key: &str) -> Option<String> {
125 let binds = |config: &ManagedConfigV1| match key {
127 "hive.url" => config.sets("hive.url"),
128 "hive.root_key" => config.sets("hive.root_key"),
129 "hive.metrics_upload" => config.telemetry_metrics_upload == Some(false),
130 "hive.receipts_upload" => config.telemetry_receipts_upload == Some(false),
131 _ => false,
132 };
133 if !matches!(
134 key,
135 "hive.url" | "hive.root_key" | "hive.metrics_upload" | "hive.receipts_upload"
136 ) {
137 return None;
138 }
139 match &self.config {
140 Err(error) => Some(format!(
141 "the managed configuration ({}) is refused ({error}); `{key}` cannot be changed until your organisation fixes it",
142 self.location
143 )),
144 Ok(config) if binds(config) => Some(format!(
145 "{}: `{key}` is set by the managed configuration ({}); a user setting cannot override it",
146 self.manager(),
147 self.location
148 )),
149 Ok(_) => None,
150 }
151 }
152
153 pub fn org_refusal(&self, org_id: &str) -> Option<String> {
155 match &self.config {
156 Err(error) => Some(format!(
157 "the managed configuration ({}) is refused: {error}",
158 self.location
159 )),
160 Ok(config) => config
161 .hive_org
162 .as_deref()
163 .filter(|managed| *managed != org_id)
164 .map(|managed| {
165 format!("this machine is managed by {managed}: it cannot connect to the org {org_id}")
166 }),
167 }
168 }
169
170 pub fn wire(&self) -> HiveStatusManagedV1 {
172 let (members, digest, error) = match &self.config {
173 Ok(config) => (
174 config
175 .members()
176 .into_iter()
177 .map(|(member, value)| HiveStatusManagedMemberV1 {
178 member: member.to_string(),
179 value,
180 })
181 .collect(),
182 Some(config.digest()),
183 None,
184 ),
185 Err(error) => (Vec::new(), None, Some(error.clone())),
186 };
187 HiveStatusManagedV1 {
188 carrier: self.carrier,
189 location: self.location.clone(),
190 members,
191 digest,
192 error,
193 }
194 }
195}
196
197type Cached = Option<(Instant, Option<Arc<Managed>>)>;
198
199pub fn current() -> Option<Arc<Managed>> {
201 static CACHE: Mutex<Cached> = Mutex::new(None);
202 #[cfg(test)]
203 if let Some(TestOverride(managed)) = TEST_MANAGED.with(|cell| cell.borrow().clone()) {
204 return managed;
205 }
206 if let Ok(guard) = CACHE.lock()
207 && let Some((at, ref managed)) = *guard
208 && at.elapsed() < REREAD_AFTER
209 {
210 return managed.clone();
211 }
212 let managed = Managed::load(&lean_ctx_fs::managed::system_carrier(), Owner::Root).map(Arc::new);
213 if let Ok(mut guard) = CACHE.lock() {
214 *guard = Some((Instant::now(), managed.clone()));
215 }
216 managed
217}
218
219#[cfg(test)]
221#[derive(Clone)]
222struct TestOverride(Option<Arc<Managed>>);
223
224#[cfg(test)]
225thread_local! {
226 static TEST_MANAGED: std::cell::RefCell<Option<TestOverride>> =
227 const { std::cell::RefCell::new(None) };
228}
229
230#[cfg(test)]
233pub(crate) fn with_managed_for_test<R>(managed: Option<Managed>, body: impl FnOnce() -> R) -> R {
234 struct Reset;
235 impl Drop for Reset {
236 fn drop(&mut self) {
237 TEST_MANAGED.with(|cell| cell.borrow_mut().take());
238 }
239 }
240 TEST_MANAGED.with(|cell| *cell.borrow_mut() = Some(TestOverride(managed.map(Arc::new))));
241 let _reset = Reset;
242 body()
243}
244
245#[cfg(test)]
247pub(crate) fn managed_from_toml_for_test(toml: &str) -> Managed {
248 let dir = tempfile::tempdir().expect("tempdir");
249 let path = dir.path().join("managed.toml");
250 std::fs::write(&path, toml).expect("write");
251 Managed::load(&Carrier::File(path), Owner::Any).expect("present")
252}
253
254pub(crate) fn cache_key() -> Option<String> {
257 current().map(|managed| match &managed.config {
258 Ok(config) => config.digest(),
259 Err(error) => format!("refused:{error}"),
260 })
261}
262
263pub fn refusal_for_key(key: &str) -> Option<String> {
265 current()?.refusal(key)
266}
267
268pub type SignedInProbe = fn() -> bool;
270
271static SIGNED_IN: OnceLock<SignedInProbe> = OnceLock::new();
272
273pub fn register_signed_in_probe(probe: SignedInProbe) {
276 let _ = SIGNED_IN.set(probe);
277}
278
279pub fn require_gate(floor_installed: bool) -> Result<(), String> {
283 match current() {
284 Some(managed) => gate(&managed, floor_installed, SIGNED_IN.get().copied()),
285 None => Ok(()),
286 }
287}
288
289fn gate(
290 managed: &Managed,
291 floor_installed: bool,
292 probe: Option<SignedInProbe>,
293) -> Result<(), String> {
294 let config = match &managed.config {
295 Ok(config) => config,
296 Err(error) => {
297 return Err(format!(
298 "the managed configuration ({}) is refused: {error}. Tools stay off until your organisation fixes it",
299 managed.location
300 ));
301 }
302 };
303 if config.hive_require != Some(true) {
304 return Ok(());
305 }
306 let signed_in = probe.is_some_and(|probe| probe());
307 if !signed_in {
308 return Err(format!(
309 "{}: sign in first (`lean-ctx hive connect`). Tools stay off until this machine holds a valid Hive lease",
310 managed.manager()
311 ));
312 }
313 if !floor_installed {
314 return Err(format!(
315 "{}: the organisation's policy is not installed yet. Tools stay off until the next Hive pull installs it (`lean-ctx hive sync`)",
316 managed.manager()
317 ));
318 }
319 Ok(())
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 fn managed(toml: &str) -> Managed {
327 managed_from_toml_for_test(toml)
328 }
329
330 const KEY: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
331
332 #[test]
333 fn managed_hive_members_win_over_the_user_and_refuse_overrides() {
334 let managed = managed(&format!(
335 "[hive]\nurl = \"https://hive.acme.example\"\nroot_key = \"{KEY}\"\norg = \"org_acme\"\n\
336 [telemetry]\nmetrics_upload = false\nreceipts_upload = true\n"
337 ));
338 let user = HiveConfig {
339 url: Some("https://hive.user.example".into()),
340 root_key: None,
341 metrics_upload: true,
342 receipts_upload: true,
343 ..HiveConfig::default()
344 };
345 let mut hive = user.clone();
346 managed.apply_to_hive(&mut hive);
347 let mut saved = hive.clone();
349 managed.keep_user_values(&mut saved, &user);
350 assert_eq!(saved, user);
351 assert_eq!(hive.url.as_deref(), Some("https://hive.acme.example"));
352 assert_eq!(hive.root_key.as_deref(), Some(KEY));
353 assert!(!hive.metrics_upload && hive.receipts_upload);
355
356 let refusal = managed.refusal("hive.url").expect("refused");
357 assert!(refusal.contains("managed by org_acme"), "{refusal}");
358 assert!(managed.refusal("hive.metrics_upload").is_some());
359 assert!(managed.refusal("hive.receipts_upload").is_none());
360 assert!(managed.refusal("hive.auto_capture").is_none());
361
362 assert!(managed.org_refusal("org_acme").is_none());
363 let other = managed.org_refusal("org_other").expect("refused");
364 assert!(other.contains("managed by org_acme"), "{other}");
365
366 let wire = managed.wire();
367 assert_eq!(wire.carrier, ManagedCarrierKindV1::LinuxFile);
368 assert_eq!(wire.members[0].member, "hive.url");
369 assert!(wire.digest.is_some() && wire.error.is_none());
370 }
371
372 #[test]
373 fn require_refuses_tools_until_signed_in_with_a_floor() {
374 let managed = managed("[hive]\nrequire = true\norg = \"org_acme\"\n");
375 let no_probe = gate(&managed, true, None).expect_err("no probe");
376 assert!(no_probe.contains("sign in first"), "{no_probe}");
377 assert!(gate(&managed, true, Some(|| false)).is_err());
378 let no_floor = gate(&managed, false, Some(|| true)).expect_err("no floor");
379 assert!(no_floor.contains("policy is not installed"), "{no_floor}");
380 assert_eq!(gate(&managed, true, Some(|| true)), Ok(()));
381
382 let relaxed = self::managed("[hive]\norg = \"org_acme\"\n");
383 assert_eq!(gate(&relaxed, false, None), Ok(()));
384 }
385
386 #[test]
387 fn a_refused_carrier_fails_closed() {
388 let broken = managed("[hive]\nrequire = \"maybe\"\n");
389 assert!(broken.config.is_err());
390 assert!(gate(&broken, true, Some(|| true)).is_err());
391 assert!(broken.refusal("hive.url").is_some());
392 assert!(broken.org_refusal("org_any").is_some());
393 let mut hive = HiveConfig::default();
394 broken.apply_to_hive(&mut hive);
395 assert_eq!(hive, HiveConfig::default());
396 assert!(broken.wire().error.is_some());
397 }
398}