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