1use std::collections::{HashMap, HashSet};
8use std::sync::Arc;
9use std::sync::RwLock;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::time::Duration;
12
13use kube::ResourceExt;
14use kube::runtime::reflector::ObjectRef;
15use kube::runtime::watcher::Event;
16use tokio::sync::Notify;
17
18use crate::crd::EphemeralAccessRequest;
19
20type NamespacedKey = (String, String);
21
22const READY_TIMEOUT: Duration = Duration::from_secs(30);
31
32#[derive(Debug, thiserror::Error)]
34#[error("ephemeral request index did not sync within {waited:?}")]
35pub struct IndexNotReady {
36 waited: Duration,
37}
38
39#[derive(Default)]
40struct IndexState {
41 objects: HashMap<ObjectRef<EphemeralAccessRequest>, Arc<EphemeralAccessRequest>>,
42 by_access_policy_name: HashMap<NamespacedKey, HashSet<ObjectRef<EphemeralAccessRequest>>>,
43 by_access_policy_uid: HashMap<NamespacedKey, HashSet<ObjectRef<EphemeralAccessRequest>>>,
44 by_target_policy_uid: HashMap<NamespacedKey, HashSet<ObjectRef<EphemeralAccessRequest>>>,
45}
46
47impl IndexState {
48 fn remove(&mut self, object_ref: &ObjectRef<EphemeralAccessRequest>) {
49 let Some(request) = self.objects.remove(object_ref) else {
50 return;
51 };
52 remove_ref(
53 &mut self.by_access_policy_name,
54 &namespaced_key(&request, &request.spec.access_policy_ref.name),
55 object_ref,
56 );
57 if let Some(resolved) = request
58 .status
59 .as_ref()
60 .and_then(|status| status.resolved_access.as_ref())
61 {
62 remove_ref(
63 &mut self.by_access_policy_uid,
64 &namespaced_key(&request, &resolved.access_policy_uid),
65 object_ref,
66 );
67 remove_ref(
68 &mut self.by_target_policy_uid,
69 &namespaced_key(&request, &resolved.target_policy_uid),
70 object_ref,
71 );
72 }
73 }
74
75 fn upsert(&mut self, request: &EphemeralAccessRequest) {
76 let object_ref = ObjectRef::from_obj(request);
77 self.remove(&object_ref);
78 let request = Arc::new(request.clone());
79 self.by_access_policy_name
80 .entry(namespaced_key(
81 &request,
82 &request.spec.access_policy_ref.name,
83 ))
84 .or_default()
85 .insert(object_ref.clone());
86 if let Some(resolved) = request
87 .status
88 .as_ref()
89 .and_then(|status| status.resolved_access.as_ref())
90 {
91 self.by_access_policy_uid
92 .entry(namespaced_key(&request, &resolved.access_policy_uid))
93 .or_default()
94 .insert(object_ref.clone());
95 self.by_target_policy_uid
96 .entry(namespaced_key(&request, &resolved.target_policy_uid))
97 .or_default()
98 .insert(object_ref.clone());
99 }
100 self.objects.insert(object_ref, request);
101 }
102
103 fn values_for(
104 &self,
105 index: &HashMap<NamespacedKey, HashSet<ObjectRef<EphemeralAccessRequest>>>,
106 key: &NamespacedKey,
107 ) -> Vec<Arc<EphemeralAccessRequest>> {
108 index
109 .get(key)
110 .into_iter()
111 .flatten()
112 .filter_map(|object_ref| self.objects.get(object_ref).cloned())
113 .collect()
114 }
115}
116
117fn namespaced_key(request: &EphemeralAccessRequest, value: &str) -> NamespacedKey {
118 (request.namespace().unwrap_or_default(), value.to_string())
119}
120
121fn remove_ref(
122 index: &mut HashMap<NamespacedKey, HashSet<ObjectRef<EphemeralAccessRequest>>>,
123 key: &NamespacedKey,
124 object_ref: &ObjectRef<EphemeralAccessRequest>,
125) {
126 let remove_key = if let Some(refs) = index.get_mut(key) {
127 refs.remove(object_ref);
128 refs.is_empty()
129 } else {
130 false
131 };
132 if remove_key {
133 index.remove(key);
134 }
135}
136
137#[derive(Clone, Default)]
139pub struct RequestIndex {
140 live: Arc<RwLock<IndexState>>,
141 initializing: Arc<RwLock<Option<IndexState>>>,
142 ready: Arc<AtomicBool>,
143 ready_notify: Arc<Notify>,
144}
145
146impl RequestIndex {
147 pub fn observe(&self, event: &Event<EphemeralAccessRequest>) {
149 match event {
150 Event::Apply(request) => self
151 .live
152 .write()
153 .expect("request index poisoned")
154 .upsert(request),
155 Event::Delete(request) => self
156 .live
157 .write()
158 .expect("request index poisoned")
159 .remove(&ObjectRef::from_obj(request)),
160 Event::Init => {
161 *self.initializing.write().expect("request index poisoned") =
162 Some(IndexState::default());
163 }
164 Event::InitApply(request) => {
165 if let Some(buffer) = self
166 .initializing
167 .write()
168 .expect("request index poisoned")
169 .as_mut()
170 {
171 buffer.upsert(request);
172 }
173 }
174 Event::InitDone => {
175 if let Some(buffer) = self
176 .initializing
177 .write()
178 .expect("request index poisoned")
179 .take()
180 {
181 *self.live.write().expect("request index poisoned") = buffer;
182 }
183 self.ready.store(true, Ordering::Release);
184 self.ready_notify.notify_waiters();
185 }
186 }
187 }
188
189 async fn wait_ready(&self) -> Result<(), IndexNotReady> {
190 let synced = async {
191 while !self.ready.load(Ordering::Acquire) {
192 let notified = self.ready_notify.notified();
195 if self.ready.load(Ordering::Acquire) {
196 break;
197 }
198 notified.await;
199 }
200 };
201 tokio::time::timeout(READY_TIMEOUT, synced)
202 .await
203 .map_err(|_| IndexNotReady {
204 waited: READY_TIMEOUT,
205 })
206 }
207
208 pub async fn for_access_policy_name(
209 &self,
210 namespace: &str,
211 name: &str,
212 ) -> Result<Vec<Arc<EphemeralAccessRequest>>, IndexNotReady> {
213 self.wait_ready().await?;
214 let state = self.live.read().expect("request index poisoned");
215 Ok(state.values_for(
216 &state.by_access_policy_name,
217 &(namespace.to_string(), name.to_string()),
218 ))
219 }
220
221 pub async fn for_access_policy_uid(
222 &self,
223 namespace: &str,
224 uid: &str,
225 ) -> Result<Vec<Arc<EphemeralAccessRequest>>, IndexNotReady> {
226 self.wait_ready().await?;
227 let state = self.live.read().expect("request index poisoned");
228 Ok(state.values_for(
229 &state.by_access_policy_uid,
230 &(namespace.to_string(), uid.to_string()),
231 ))
232 }
233
234 pub async fn for_target_policy_uid(
235 &self,
236 namespace: &str,
237 uid: &str,
238 ) -> Result<Vec<Arc<EphemeralAccessRequest>>, IndexNotReady> {
239 self.wait_ready().await?;
240 let state = self.live.read().expect("request index poisoned");
241 Ok(state.values_for(
242 &state.by_target_policy_uid,
243 &(namespace.to_string(), uid.to_string()),
244 ))
245 }
246
247 pub fn len(&self) -> usize {
248 self.live
249 .read()
250 .expect("request index poisoned")
251 .objects
252 .len()
253 }
254
255 pub fn is_empty(&self) -> bool {
256 self.len() == 0
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::crd::{
264 EphemeralAccessActor, EphemeralAccessRequestSpec, EphemeralAccessRequestStatus,
265 EphemeralAccessSubject, LocalObjectReference, ResolvedEphemeralAccess,
266 };
267
268 fn request(
269 name: &str,
270 access_name: &str,
271 access_uid: &str,
272 target_uid: &str,
273 ) -> EphemeralAccessRequest {
274 let mut request = EphemeralAccessRequest::new(
275 name,
276 EphemeralAccessRequestSpec {
277 access_policy_ref: LocalObjectReference {
278 name: access_name.into(),
279 },
280 subject: EphemeralAccessSubject {
281 role: "alice".into(),
282 },
283 requested_by: EphemeralAccessActor {
284 username: "user".into(),
285 uid: None,
286 groups: vec![],
287 },
288 requested_duration: None,
289 justification: None,
290 },
291 );
292 request.metadata.namespace = Some("ns".into());
293 if !access_uid.is_empty() {
294 request.status = Some(EphemeralAccessRequestStatus {
295 resolved_access: Some(ResolvedEphemeralAccess {
296 access_policy_uid: access_uid.into(),
297 access_policy_generation: 1,
298 target_policy_uid: target_uid.into(),
299 target_policy_generation: 1,
300 target_database_fingerprint: "sha256:test".into(),
301 granted_duration: "1h".into(),
302 bundle_encoding: "test".into(),
303 bundle_hash: "sha256:test".into(),
304 memberships: vec![],
305 }),
306 ..Default::default()
307 });
308 }
309 request
310 }
311
312 #[tokio::test]
313 async fn indexes_names_and_resolved_uids_and_replaces_on_restart() {
314 let index = RequestIndex::default();
315 let first = request("one", "access", "access-uid", "target-uid");
316 index.observe(&Event::Init);
317 index.observe(&Event::InitApply(first.clone()));
318 index.observe(&Event::InitDone);
319 assert_eq!(
320 index
321 .for_access_policy_name("ns", "access")
322 .await
323 .unwrap()
324 .len(),
325 1
326 );
327 assert_eq!(
328 index
329 .for_access_policy_uid("ns", "access-uid")
330 .await
331 .unwrap()
332 .len(),
333 1
334 );
335 assert_eq!(
336 index
337 .for_target_policy_uid("ns", "target-uid")
338 .await
339 .unwrap()
340 .len(),
341 1
342 );
343
344 index.observe(&Event::Init);
345 index.observe(&Event::InitDone);
346 assert_eq!(index.len(), 0);
347 }
348
349 #[tokio::test(start_paused = true)]
350 async fn lookups_fail_instead_of_hanging_when_the_watch_never_syncs() {
351 let index = RequestIndex::default();
356 index.observe(&Event::Init);
357 assert!(
358 index
359 .for_target_policy_uid("ns", "target-uid")
360 .await
361 .is_err()
362 );
363 }
364
365 #[tokio::test(start_paused = true)]
366 async fn lookups_unblock_as_soon_as_the_watch_syncs() {
367 let index = RequestIndex::default();
368 let waiter = {
369 let index = index.clone();
370 tokio::spawn(async move { index.for_access_policy_name("ns", "access").await })
371 };
372 tokio::task::yield_now().await;
373 index.observe(&Event::Init);
374 index.observe(&Event::InitApply(request("one", "access", "", "")));
375 index.observe(&Event::InitDone);
376 assert_eq!(waiter.await.expect("waiter panicked").unwrap().len(), 1);
377 }
378
379 #[tokio::test]
380 async fn unresolved_requests_are_indexed_by_policy_name() {
381 let index = RequestIndex::default();
382 index.observe(&Event::Init);
383 index.observe(&Event::InitApply(request("one", "access", "", "")));
384 index.observe(&Event::InitDone);
385 assert_eq!(
386 index
387 .for_access_policy_name("ns", "access")
388 .await
389 .unwrap()
390 .len(),
391 1
392 );
393 assert!(
394 index
395 .for_access_policy_uid("ns", "access-uid")
396 .await
397 .unwrap()
398 .is_empty()
399 );
400 }
401
402 #[tokio::test]
403 async fn lookup_ignores_irrelevant_requests_and_forged_labels() {
404 let index = RequestIndex::default();
405 index.observe(&Event::Init);
406 for sequence in 0..1_000 {
407 let mut irrelevant = request(
408 &format!("irrelevant-{sequence}"),
409 "other-access",
410 "other-access-uid",
411 "other-target-uid",
412 );
413 irrelevant.metadata.namespace = Some(if sequence % 2 == 0 {
414 "ns".into()
415 } else {
416 "other-ns".into()
417 });
418 index.observe(&Event::InitApply(irrelevant));
419 }
420 let mut relevant = request("relevant", "access", "access-uid", "target-uid");
421 relevant.labels_mut().insert(
422 crate::crd::LABEL_TARGET_POLICY_UID.into(),
423 "forged-uid".into(),
424 );
425 index.observe(&Event::InitApply(relevant));
426 index.observe(&Event::InitDone);
427
428 let matches = index
429 .for_target_policy_uid("ns", "target-uid")
430 .await
431 .unwrap();
432 assert_eq!(matches.len(), 1);
433 assert_eq!(matches[0].name_any(), "relevant");
434 assert!(
435 index
436 .for_target_policy_uid("ns", "forged-uid")
437 .await
438 .unwrap()
439 .is_empty()
440 );
441 }
442}