elasticctl_api/value_lists/
scan.rs1use elasticctl_core::{Error, ErrorKind, Feature, Result, Transport, urlencode};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use super::{
6 ValueListType,
7 reads::{malformed, required_string},
8};
9use crate::{
10 exceptions,
11 model::{ExceptionItem, ListKey},
12};
13
14#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ReferenceOperator {
17 Included,
18 Excluded,
19}
20
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22pub struct InstalledReference {
23 pub container: ListKey,
24 pub item_id: String,
25 pub field: String,
26 pub operator: ReferenceOperator,
27 pub list_id: String,
28 pub list_type: ValueListType,
29}
30
31impl InstalledReference {
32 pub fn name(&self) -> String {
33 format!(
34 "{} ({}) item {} field {} {:?}",
35 self.container.list_id,
36 self.container.namespace_type,
37 self.item_id,
38 self.field,
39 self.operator
40 )
41 }
42}
43
44pub fn item_references(key: &ListKey, item: &ExceptionItem) -> Result<Vec<InstalledReference>> {
45 let body = Value::Object(item.as_map().clone());
46 if required_string(&body, "list_id")? != key.list_id
47 || required_string(&body, "namespace_type")? != key.namespace_type
48 {
49 return Err(malformed(
50 "exception item identity does not match its container",
51 ));
52 }
53 let item_id = required_string(&body, "item_id")?;
54 let entries = body
55 .get("entries")
56 .and_then(Value::as_array)
57 .ok_or_else(|| malformed(format!("exception item {item_id} has unreadable entries")))?;
58 let mut references = Vec::new();
59 for entry in entries {
60 let kind = required_string(entry, "type")?;
61 if kind != "list" {
62 continue;
63 }
64 let operator = match required_string(entry, "operator")? {
65 "included" => ReferenceOperator::Included,
66 "excluded" => ReferenceOperator::Excluded,
67 _ => {
68 return Err(malformed(format!(
69 "exception item {item_id} has an unreadable list operator"
70 )));
71 }
72 };
73 let list = entry.get("list").ok_or_else(|| {
74 malformed(format!(
75 "exception item {item_id} has an unreadable list entry"
76 ))
77 })?;
78 references.push(InstalledReference {
79 container: key.clone(),
80 item_id: item_id.into(),
81 field: required_string(entry, "field")?.into(),
82 operator,
83 list_id: required_string(list, "id")?.into(),
84 list_type: required_string(list, "type")?.to_owned().into(),
85 });
86 }
87 Ok(references)
88}
89
90pub(super) fn sort_references(references: &mut [InstalledReference]) {
91 references.sort_by_cached_key(|r| {
92 (
93 r.container.clone(),
94 r.item_id.clone(),
95 r.field.clone(),
96 r.operator,
97 r.list_id.clone(),
98 r.list_type.to_string(),
99 )
100 });
101}
102
103pub async fn scan_references(t: &Transport) -> Result<Vec<InstalledReference>> {
104 t.require_feature(Feature::ExceptionLists).await?;
105 let mut references = Vec::new();
106 for namespace in ["single", "agnostic"] {
107 scan_namespace(t, namespace, &mut references).await?;
108 }
109 sort_references(&mut references);
110 Ok(references)
111}
112
113pub(super) async fn scan_namespace(
114 t: &Transport,
115 namespace: &str,
116 references: &mut Vec<InstalledReference>,
117) -> Result<()> {
118 let body = t
119 .get(&format!(
120 "/api/exception_lists/_find?page=1&per_page=10000&namespace_type={namespace}"
121 ))
122 .await?;
123 let lists = decode_scan_page(&body)?;
124 let mut seen = std::collections::BTreeSet::new();
125 for list in lists {
126 let key = ListKey {
127 list_id: required_string(&list, "list_id")?.into(),
128 namespace_type: required_string(&list, "namespace_type")?.into(),
129 };
130 if key.namespace_type != namespace || !seen.insert(key.clone()) {
131 return Err(malformed(
132 "invalid or duplicated exception container in reference scan",
133 ));
134 }
135 let read = exceptions::get_list(t, &key).await?;
136 if read.key()? != key {
137 return Err(malformed(
138 "exception container changed identity during reference scan",
139 ));
140 }
141 let body = t.get(&format!("/api/exception_lists/items/_find?list_id={}&namespace_type={}&page=1&per_page=10000", urlencode(&key.list_id), urlencode(&key.namespace_type))).await?;
142 let items = decode_scan_page(&body)?;
143 let mut seen_items = std::collections::BTreeSet::new();
144 for item in items {
145 let item = ExceptionItem::from_value(item)?;
146 if !seen_items.insert(item.item_id()?.to_owned()) {
147 return Err(malformed("duplicate exception item in reference scan"));
148 }
149 references.extend(item_references(&key, &item)?);
150 }
151 }
152 Ok(())
153}
154
155pub(super) fn decode_scan_page(body: &Value) -> Result<Vec<Value>> {
156 let total = body
157 .get("total")
158 .and_then(Value::as_u64)
159 .ok_or_else(|| malformed("invalid reference scan total"))?;
160 if total > 10_000 {
161 return Err(Error::new(
162 ErrorKind::Unsupported,
163 "reference scan exceeds the 10,000-object read window",
164 ));
165 }
166 let data = body
167 .get("data")
168 .and_then(Value::as_array)
169 .ok_or_else(|| malformed("invalid reference scan data"))?;
170 let per_page = body
171 .get("per_page")
172 .and_then(Value::as_u64)
173 .ok_or_else(|| malformed("invalid reference scan page size"))?;
174 if body.get("page").and_then(Value::as_u64) != Some(1)
175 || per_page == 0
176 || per_page > 10_000
177 || total > per_page
178 || data.len() as u64 != total
179 {
180 return Err(malformed(
181 "refusing incomplete or inconsistent reference scan page",
182 ));
183 }
184 Ok(data.clone())
185}