1use rmcp::{ErrorData as McpError, model::*};
2
3use crate::{
4 control_plane::policy::Policy, core::dns::service::DnsService, core::dns::settings,
5 mcp::helpers::run_json,
6};
7
8pub async fn handle_get_settings<C: DnsService + Send + Sync>(
9 client: &C,
10 policy: &Policy,
11 show_secrets: bool,
12) -> Result<CallToolResult, McpError> {
13 Ok(
14 run_json("dns_get_settings", policy.check_read(), async move {
15 if show_secrets {
16 settings::get_settings_unredacted(client).await
17 } else {
18 settings::get_settings(client).await
19 }
20 })
21 .await,
22 )
23}
24
25#[cfg(test)]
26mod tests {
27 use serde_json::{Value, json};
28
29 use super::*;
30 use crate::{
31 control_plane::{
32 config::VendorKind,
33 policy::{Policy, PolicyRule},
34 },
35 core::{
36 dns::{
37 capabilities::VendorCapabilities,
38 logs::{LogLine, LogsOptions, LogsRead},
39 records::RecordData,
40 responses::ListRecordsResponse,
41 service::{
42 AccessListRead, AccessListWrite, CacheRead, CacheWrite, DnsVendor,
43 ListRecordsOptions, RecordWrite, SettingsRead, StatsRead, ZoneExport,
44 ZoneImport, ZoneRead, ZoneWrite,
45 },
46 },
47 error::Result,
48 redaction::REDACTED_MARKER,
49 },
50 };
51
52 struct FakeDnsService {
61 settings: Value,
62 }
63
64 impl DnsVendor for FakeDnsService {
65 fn kind(&self) -> VendorKind {
66 VendorKind::Technitium
67 }
68
69 fn capabilities(&self) -> VendorCapabilities {
70 VendorCapabilities {
71 settings: true,
72 ..VendorCapabilities::default()
73 }
74 }
75 }
76
77 impl ZoneRead for FakeDnsService {
78 async fn list_zones(&self, _page: u32, _per_page: u32) -> Result<Value> {
79 unreachable!("not used by settings handler")
80 }
81
82 async fn list_records(
83 &self,
84 _domain: &str,
85 _zone: Option<&str>,
86 _options: ListRecordsOptions,
87 ) -> Result<ListRecordsResponse> {
88 unreachable!("not used by settings handler")
89 }
90 }
91
92 impl ZoneWrite for FakeDnsService {
93 async fn create_zone(&self, _zone: &str, _zone_type: &str) -> Result<Value> {
94 unreachable!("not used by settings handler")
95 }
96
97 async fn delete_zone(&self, _zone: &str) -> Result<Value> {
98 unreachable!("not used by settings handler")
99 }
100
101 async fn enable_zone(&self, _zone: &str) -> Result<Value> {
102 unreachable!("not used by settings handler")
103 }
104
105 async fn disable_zone(&self, _zone: &str) -> Result<Value> {
106 unreachable!("not used by settings handler")
107 }
108 }
109
110 impl RecordWrite for FakeDnsService {
111 async fn add_record(
112 &self,
113 _zone: &str,
114 _domain: &str,
115 _ttl: u32,
116 _record: &RecordData,
117 ) -> Result<Value> {
118 unreachable!("not used by settings handler")
119 }
120
121 async fn delete_record(
122 &self,
123 _zone: &str,
124 _domain: &str,
125 _type_params: &[(&str, String)],
126 ) -> Result<Value> {
127 unreachable!("not used by settings handler")
128 }
129 }
130
131 impl CacheRead for FakeDnsService {
132 async fn list_cache(&self, _domain: &str) -> Result<Value> {
133 unreachable!("not used by settings handler")
134 }
135 }
136
137 impl CacheWrite for FakeDnsService {
138 async fn delete_cache_zone(&self, _domain: &str) -> Result<Value> {
139 unreachable!("not used by settings handler")
140 }
141
142 async fn flush_cache(&self) -> Result<Value> {
143 unreachable!("not used by settings handler")
144 }
145 }
146
147 impl AccessListRead for FakeDnsService {
148 async fn list_blocked(&self) -> Result<Value> {
149 unreachable!("not used by settings handler")
150 }
151
152 async fn list_allowed(&self) -> Result<Value> {
153 unreachable!("not used by settings handler")
154 }
155 }
156
157 impl AccessListWrite for FakeDnsService {
158 async fn add_blocked(&self, _domain: &str) -> Result<Value> {
159 unreachable!("not used by settings handler")
160 }
161
162 async fn delete_blocked(&self, _domain: &str) -> Result<Value> {
163 unreachable!("not used by settings handler")
164 }
165
166 async fn add_allowed(&self, _domain: &str) -> Result<Value> {
167 unreachable!("not used by settings handler")
168 }
169
170 async fn delete_allowed(&self, _domain: &str) -> Result<Value> {
171 unreachable!("not used by settings handler")
172 }
173 }
174
175 impl StatsRead for FakeDnsService {
176 async fn get_stats(&self, _stats_type: &str) -> Result<Value> {
177 unreachable!("not used by settings handler")
178 }
179 }
180
181 impl ZoneImport for FakeDnsService {
182 async fn import_zone_file(
183 &self,
184 _zone: &str,
185 _file_name: String,
186 _file_bytes: Vec<u8>,
187 _overwrite: bool,
188 _overwrite_zone: bool,
189 _overwrite_soa_serial: bool,
190 ) -> Result<Value> {
191 unreachable!("not used by settings handler")
192 }
193 }
194
195 impl ZoneExport for FakeDnsService {
196 async fn export_zone_file(&self, _zone: &str) -> Result<String> {
197 unreachable!("not used by settings handler")
198 }
199 }
200
201 impl SettingsRead for FakeDnsService {
202 async fn get_settings(&self) -> Result<Value> {
203 Ok(self.settings.clone())
204 }
205 }
206
207 impl LogsRead for FakeDnsService {
208 async fn get_logs(&self, _options: LogsOptions) -> Result<Vec<LogLine>> {
209 unreachable!("not used by settings handler")
210 }
211 }
212
213 #[tokio::test]
214 async fn handle_get_settings_returns_redacted_json() {
215 let client = FakeDnsService {
216 settings: json!({
217 "version": "13.4.1",
218 "tsigKeys": [{ "sharedSecret": "actual-secret" }]
219 }),
220 };
221 let policy = Policy::new([PolicyRule::Read], None);
222
223 let result = handle_get_settings(&client, &policy, false).await.unwrap();
224 let text = result.content[0]
225 .as_text()
226 .expect("settings result should be text JSON");
227 let value: Value = serde_json::from_str(&text.text).unwrap();
228
229 assert_eq!(value["version"], "13.4.1");
230 assert_eq!(value["tsigKeys"][0]["sharedSecret"], REDACTED_MARKER);
231 }
232
233 #[tokio::test]
234 async fn handle_get_settings_can_return_unredacted_json() {
235 let client = FakeDnsService {
236 settings: json!({
237 "version": "13.4.1",
238 "tsigKeys": [{ "sharedSecret": "actual-secret" }]
239 }),
240 };
241 let policy = Policy::new([PolicyRule::Read], None);
242
243 let result = handle_get_settings(&client, &policy, true).await.unwrap();
244 let text = result.content[0]
245 .as_text()
246 .expect("settings result should be text JSON");
247 let value: Value = serde_json::from_str(&text.text).unwrap();
248
249 assert_eq!(value["version"], "13.4.1");
250 assert_eq!(value["tsigKeys"][0]["sharedSecret"], "actual-secret");
251 }
252
253 #[tokio::test]
254 async fn handle_get_settings_denies_without_read_policy() {
255 let client = FakeDnsService {
256 settings: json!({
257 "version": "13.4.1",
258 "tsigKeys": [{ "sharedSecret": "actual-secret" }]
259 }),
260 };
261 let policy = Policy::new([PolicyRule::Write], None);
262
263 let result = handle_get_settings(&client, &policy, false).await.unwrap();
264 let text = result.content[0]
265 .as_text()
266 .expect("policy denial should be returned as text JSON");
267
268 assert_eq!(result.is_error, Some(true));
269 assert!(!text.text.contains("actual-secret"));
270 assert!(!text.text.contains("tsigKeys"));
271 assert!(text.text.contains("does not permit read operations"));
272 }
273}