Skip to main content

dnslib/control_plane/
policy.rs

1//! Guardrail policy for the MCP server.
2//!
3//! Policy is evaluated before any tool call dispatches to `dns::*`.
4//! Config, CLI, and env vars are the source of truth — callers of `DnsServer::new`
5//! must construct a `Policy` for the selected DNS server and pass it in.
6//!
7//! # Operation sets
8//!
9//! A `Policy` holds an explicit set of allowed `PolicyRule` variants.
10//! Rules are independent: you can permit any combination of Read, Write, and Delete.
11//!
12//! - **Read**: list/export/stats/settings/cache-browse tools are permitted.
13//! - **Write**: create/update/import/flush/block/allow tools are permitted.
14//! - **Delete**: delete tools are permitted.
15//! - **Zone allow-list**: any tool that targets a specific zone is rejected unless
16//!   that zone (or its parent) is in the allow-list. Zone-agnostic tools (stats,
17//!   settings, cache browse) are always permitted.
18
19use std::collections::HashSet;
20
21use clap::ValueEnum;
22use serde::{Deserialize, Serialize};
23
24use crate::cli::Cli;
25use crate::control_plane::config::{AppConfig, McpPermissions};
26use crate::core::error::{Error, Result};
27
28/// Identifies a single class of DNS operation.
29///
30/// A `Policy` holds a `HashSet<PolicyRule>` — only operations whose rule is
31/// present in that set are permitted.
32#[derive(
33    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ValueEnum,
34)]
35#[serde(rename_all = "lowercase")]
36pub enum PolicyRule {
37    /// Read-only operations: list zones/records, export, stats, settings, cache browse.
38    Read,
39    /// Write operations: create/update/import/flush/block/allow.
40    Write,
41    /// Delete operations: delete zone/record/cache/block/allow entries.
42    Delete,
43}
44
45/// Governs what the MCP server is permitted to do.
46#[derive(Debug, Clone)]
47pub struct Policy {
48    /// Set of permitted operation classes.
49    pub allowed: HashSet<PolicyRule>,
50
51    /// If `Some`, only zones in this list (case-insensitive) are accessible.
52    /// `None` means unrestricted.
53    pub allowed_zones: Option<Vec<String>>,
54}
55
56impl Default for Policy {
57    fn default() -> Self {
58        Self::new([PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete], None)
59    }
60}
61
62impl Policy {
63    /// Construct a new policy from its constituent parts.
64    pub fn new(
65        allowed: impl IntoIterator<Item = PolicyRule>,
66        allowed_zones: Option<Vec<String>>,
67    ) -> Self {
68        Self {
69            allowed: allowed.into_iter().collect(),
70            allowed_zones: allowed_zones
71                .map(|zones| zones.into_iter().map(|z| z.to_lowercase()).collect()),
72        }
73    }
74
75    pub fn check(&self, rule: PolicyRule) -> Result<()> {
76        if self.allowed.contains(&rule) {
77            return Ok(());
78        }
79        match rule {
80            PolicyRule::Read => {
81                tracing::warn!("read rejected: read is not in the allowed set");
82                Err(Error::policy_violation(
83                    "this MCP server does not permit read operations",
84                    "Update this server's MCP permissions or add 'read' to the allowed operations.",
85                ))
86            }
87            PolicyRule::Write => {
88                tracing::warn!("write rejected: write is not in the allowed set");
89                Err(Error::policy_violation(
90                    "this MCP server does not permit write operations",
91                    "Update this server's MCP permissions or add 'write' to the allowed operations.",
92                ))
93            }
94            PolicyRule::Delete => {
95                tracing::warn!("delete rejected: delete is not in the allowed set");
96                Err(Error::policy_violation(
97                    "this MCP server does not permit delete operations",
98                    "Update this server's MCP permissions or add 'delete' to the allowed operations.",
99                ))
100            }
101        }
102    }
103
104    /// Assert that the active policy permits read operations.
105    /// Shorthand for `check(PolicyRule::Read)`.
106    pub fn check_read(&self) -> Result<()> {
107        self.check(PolicyRule::Read)
108    }
109
110    /// Assert that the active policy permits write operations.
111    /// Shorthand for `check(PolicyRule::Write)`.
112    pub fn check_write(&self) -> Result<()> {
113        self.check(PolicyRule::Write)
114    }
115
116    pub fn check_delete(&self) -> Result<()> {
117        self.check(PolicyRule::Delete)
118    }
119
120    pub fn check_zone(&self, zone: &str) -> Result<()> {
121        let Some(allowed_zones) = &self.allowed_zones else {
122            return Ok(());
123        };
124
125        let zone = zone.trim_end_matches('.').to_lowercase();
126        let allowed = allowed_zones.iter().any(|allowed| {
127            let allowed = allowed.trim_end_matches('.').to_lowercase();
128            zone == allowed || zone.ends_with(&format!(".{allowed}"))
129        });
130
131        if allowed {
132            Ok(())
133        } else {
134            Err(Error::policy_violation(
135                format!("zone '{zone}' is outside the configured allowed zones"),
136                "Choose a zone permitted by this server's policy.",
137            ))
138        }
139    }
140
141    /// Returns a human-readable summary of active restrictions, used in the
142    /// MCP `ServerInfo.instructions` field so Claude knows upfront what it can do.
143    pub fn instructions_suffix(&self) -> String {
144        let mut parts = Vec::new();
145
146        // Collect disabled operations (those NOT in self.allowed)
147        let mut disabled: Vec<&str> = Vec::new();
148        if !self.allowed.contains(&PolicyRule::Read) {
149            disabled.push("read");
150        }
151        if !self.allowed.contains(&PolicyRule::Write) {
152            disabled.push("write");
153        }
154        if !self.allowed.contains(&PolicyRule::Delete) {
155            disabled.push("delete");
156        }
157
158        if !disabled.is_empty() {
159            // Check for common named combinations for human-friendly messages
160            let read_disabled = disabled.contains(&"read");
161            let write_disabled = disabled.contains(&"write");
162            let delete_disabled = disabled.contains(&"delete");
163
164            if read_disabled && write_disabled && !delete_disabled {
165                // only delete allowed — unusual but possible
166                parts.push("⚠️  Restricted mode: read and write operations are disabled.".to_string());
167            } else if read_disabled && delete_disabled && !write_disabled {
168                // write-only
169                parts.push(
170                    "⚠️  Write-only mode: read and delete operations are disabled.".to_string(),
171                );
172            } else if write_disabled && delete_disabled && !read_disabled {
173                // read-only
174                parts.push(
175                    "⚠️  Read-only mode: all write and delete operations are disabled.".to_string(),
176                );
177            } else if read_disabled && !write_disabled && !delete_disabled {
178                // write+delete mode (read disabled) — write mode with read blocked
179                parts.push(
180                    "⚠️  Write mode: read operations are disabled.".to_string(),
181                );
182            } else if delete_disabled && !read_disabled && !write_disabled {
183                // read+write mode (delete disabled) — write mode without deletes
184                parts.push(
185                    "⚠️  Write mode: delete operations are disabled.".to_string(),
186                );
187            } else {
188                // Generic fallback: list the disabled operations
189                parts.push(format!(
190                    "⚠️  Restricted mode: {} operations are disabled.",
191                    disabled.join(", ")
192                ));
193            }
194        }
195
196        if let Some(ref zones) = self.allowed_zones {
197            parts.push(format!(
198                "⚠️  Zone restriction: only the following zones are accessible: {}.",
199                zones.join(", ")
200            ));
201        }
202
203        if parts.is_empty() {
204            String::new()
205        } else {
206            format!("\n\n{}", parts.join("\n"))
207        }
208    }
209}
210
211impl Policy {
212    /// Constructs an effective `Policy` for a single DNS server by combining the server's MCP
213    /// access configuration with CLI-provided access and zone overrides.
214    ///
215    /// - Operation permissions: if `cli_access` is empty the server's MCP `access` is used;
216    ///   otherwise the resulting allowed operations are the intersection of `cli_access` and the
217    ///   server's MCP `access` (the CLI cannot broaden permissions beyond the server's config).
218    /// - Zone restrictions: if `cli_allow_zone` is empty the server's configured `allowed_zones`
219    ///   (if any) is used; if `cli_allow_zone` is non-empty it becomes the resulting zone list.
220    ///   When the server has configured allowed zones, each CLI-provided zone is validated against
221    ///   the server's allowed zones (subdomains and case-insensitive matches are permitted);
222    ///   a CLI zone outside the server's configured zones causes a `PolicyViolation` error.
223    ///
224    /// # Errors
225    ///
226    /// Returns `Error::PolicyViolation` when any entry in `cli_allow_zone` is not permitted by the
227    /// server's MCP configured allowed zones (when that restriction exists).
228    ///
229    /// # Examples
230    ///
231    /// ```ignore
232    /// use crate::control_plane::policy::Policy;
233    /// use crate::control_plane::config::DnsServerConfig;
234    ///
235    /// // Construct `server`, `cli_access`, and `cli_allow_zone` according to your application.
236    /// let server: DnsServerConfig = /* server from config */ unimplemented!();
237    /// let cli_access = vec![]; // empty means "use server MCP access"
238    /// let cli_allow_zone: Vec<String> = vec![]; // empty means "use server MCP zones"
239    ///
240    /// let policy = Policy::for_server(&server, &cli_access, &cli_allow_zone)?;
241    /// ```ignore
242    pub fn for_server(
243        server: &crate::control_plane::config::DnsServerConfig,
244        cli_access: &[PolicyRule],
245        cli_allow_zone: &[String],
246    ) -> Result<Self> {
247        let mcp = &server.mcp;
248
249        let config_set: HashSet<PolicyRule> = mcp.access.iter().cloned().collect();
250        let cli_set: HashSet<PolicyRule> = cli_access.iter().cloned().collect();
251
252        let allowed: HashSet<PolicyRule> = if cli_set.is_empty() {
253            config_set
254        } else {
255            cli_set.intersection(&config_set).cloned().collect()
256        };
257
258        let configured_zones =
259            (!mcp.allowed_zones.is_empty()).then_some(&mcp.allowed_zones);
260
261        let allowed_zones = if cli_allow_zone.is_empty() {
262            configured_zones.cloned()
263        } else if let Some(configured) = configured_zones {
264            let configured_policy = Self::new(
265                [PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete],
266                Some(configured.clone()),
267            );
268            for zone in cli_allow_zone {
269                configured_policy.check_zone(zone).map_err(|_| {
270                    Error::policy_violation(
271                        format!(
272                            "--allow-zone '{zone}' is outside this server's configured MCP allowed zones"
273                        ),
274                        "Remove the override or choose a zone already permitted by this server's config.",
275                    )
276                })?;
277            }
278            Some(cli_allow_zone.to_vec())
279        } else {
280            Some(cli_allow_zone.to_vec())
281        };
282
283        Ok(Self::new(allowed, allowed_zones))
284    }
285
286    /// Build a `Policy` from CLI options and config.
287    pub fn from_cli_and_config(cli: &Cli, config: Option<&AppConfig>) -> Result<Self> {
288        let mcp = config
289            .and_then(|c| {
290                c.selected_server(cli.servers.first().map(|s| s.as_str()))
291                    .ok()
292            })
293            .map(|s| &s.mcp);
294
295        let config_set: HashSet<PolicyRule> = mcp
296            .map(|p| p.access.iter().cloned().collect())
297            .unwrap_or_else(|| {
298                [PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete]
299                    .into_iter()
300                    .collect()
301            });
302
303        let cli_set: HashSet<PolicyRule> = cli.access.iter().cloned().collect();
304
305        let allowed: HashSet<PolicyRule> = if cli_set.is_empty() {
306            config_set
307        } else {
308            cli_set.intersection(&config_set).cloned().collect()
309        };
310
311        let allowed_zones = Self::allowed_zones_from_cli_and_mcp(cli, mcp)?;
312        Ok(Self::new(allowed, allowed_zones))
313    }
314
315    /// Build allowed zones from CLI and MCP config.
316    pub fn allowed_zones_from_cli_and_mcp(
317        cli: &Cli,
318        mcp: Option<&McpPermissions>,
319    ) -> Result<Option<Vec<String>>> {
320        let configured = mcp.and_then(|permissions| {
321            (!permissions.allowed_zones.is_empty()).then_some(&permissions.allowed_zones)
322        });
323
324        if cli.allow_zone.is_empty() {
325            return Ok(configured.cloned());
326        }
327
328        let Some(configured) = configured else {
329            return Ok(Some(cli.allow_zone.clone()));
330        };
331
332        let configured_policy = Self::new(
333            [PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete],
334            Some(configured.clone()),
335        );
336        for zone in &cli.allow_zone {
337            configured_policy.check_zone(zone).map_err(|_| {
338                Error::policy_violation(
339                    format!(
340                        "--allow-zone '{zone}' is outside this server's configured MCP allowed zones"
341                    ),
342                    "Remove the override or choose a zone already permitted by this server's config.",
343                )
344            })?;
345        }
346
347        Ok(Some(cli.allow_zone.clone()))
348    }
349}
350
351// ─── Tests ────────────────────────────────────────────────────────────────────
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use rstest::{fixture, rstest};
357
358    #[fixture]
359    fn unrestricted() -> Policy {
360        Policy::new([PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete], None)
361    }
362
363    #[fixture]
364    fn readonly() -> Policy {
365        Policy::new([PolicyRule::Read], None)
366    }
367
368    #[fixture]
369    fn write_access() -> Policy {
370        Policy::new([PolicyRule::Read, PolicyRule::Write], None)
371    }
372
373    #[fixture]
374    fn write_only() -> Policy {
375        Policy::new([PolicyRule::Write], None)
376    }
377
378    #[fixture]
379    fn write_delete() -> Policy {
380        Policy::new([PolicyRule::Write, PolicyRule::Delete], None)
381    }
382
383    #[fixture]
384    fn zone_restricted() -> Policy {
385        Policy::new(
386            [PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete],
387            Some(vec!["example.com".into(), "internal.lan".into()]),
388        )
389    }
390
391    #[fixture]
392    fn both() -> Policy {
393        Policy::new([PolicyRule::Read], Some(vec!["example.com".into()]))
394    }
395
396    // ── check / check_read / check_write / check_delete ──────────────────────
397
398    #[rstest]
399    fn unrestricted_allows_reads(unrestricted: Policy) {
400        assert!(unrestricted.check_read().is_ok());
401    }
402
403    #[rstest]
404    fn unrestricted_allows_writes(unrestricted: Policy) {
405        assert!(unrestricted.check_write().is_ok());
406    }
407
408    #[rstest]
409    fn unrestricted_allows_deletes(unrestricted: Policy) {
410        assert!(unrestricted.check_delete().is_ok());
411    }
412
413    #[rstest]
414    fn readonly_allows_reads(readonly: Policy) {
415        assert!(readonly.check_read().is_ok());
416    }
417
418    #[rstest]
419    fn readonly_blocks_writes(readonly: Policy) {
420        let err = readonly.check_write().unwrap_err();
421        assert!(matches!(err, Error::PolicyViolation { .. }));
422    }
423
424    #[rstest]
425    fn readonly_blocks_deletes(readonly: Policy) {
426        assert!(readonly.check_delete().is_err());
427    }
428
429    #[rstest]
430    fn write_access_allows_writes(write_access: Policy) {
431        assert!(write_access.check_write().is_ok());
432    }
433
434    #[rstest]
435    fn write_access_blocks_deletes(write_access: Policy) {
436        let err = write_access.check_delete().unwrap_err();
437        assert!(matches!(err, Error::PolicyViolation { .. }));
438    }
439
440    #[rstest]
441    fn write_only_blocks_reads(write_only: Policy) {
442        let err = write_only.check_read().unwrap_err();
443        assert!(matches!(err, Error::PolicyViolation { .. }));
444        assert!(err.to_string().contains("read"));
445    }
446
447    #[rstest]
448    fn write_only_allows_writes(write_only: Policy) {
449        assert!(write_only.check_write().is_ok());
450    }
451
452    #[rstest]
453    fn write_only_blocks_deletes(write_only: Policy) {
454        let err = write_only.check_delete().unwrap_err();
455        assert!(matches!(err, Error::PolicyViolation { .. }));
456    }
457
458    #[rstest]
459    fn write_delete_allows_writes(write_delete: Policy) {
460        assert!(write_delete.check_write().is_ok());
461    }
462
463    #[rstest]
464    fn write_delete_allows_deletes(write_delete: Policy) {
465        assert!(write_delete.check_delete().is_ok());
466    }
467
468    #[rstest]
469    fn write_delete_blocks_reads(write_delete: Policy) {
470        let err = write_delete.check_read().unwrap_err();
471        assert!(matches!(err, Error::PolicyViolation { .. }));
472        assert!(err.to_string().contains("read"));
473    }
474
475    #[rstest]
476    fn zone_restricted_allows_writes(zone_restricted: Policy) {
477        assert!(zone_restricted.check_write().is_ok());
478    }
479
480    #[rstest]
481    fn zone_restricted_allows_deletes(zone_restricted: Policy) {
482        assert!(zone_restricted.check_delete().is_ok());
483    }
484
485    #[rstest]
486    fn both_blocks_writes(both: Policy) {
487        assert!(both.check_write().is_err());
488    }
489
490    // ── check_zone ────────────────────────────────────────────────────────────
491
492    #[rstest]
493    fn unrestricted_allows_any_zone(unrestricted: Policy) {
494        assert!(unrestricted.check_zone("anything.example.com").is_ok());
495        assert!(unrestricted.check_zone("other.net").is_ok());
496    }
497
498    #[rstest]
499    fn exact_zone_match_is_allowed(zone_restricted: Policy) {
500        assert!(zone_restricted.check_zone("example.com").is_ok());
501        assert!(zone_restricted.check_zone("internal.lan").is_ok());
502    }
503
504    #[rstest]
505    fn subdomain_of_allowed_zone_is_allowed(zone_restricted: Policy) {
506        assert!(zone_restricted.check_zone("sub.example.com").is_ok());
507        assert!(zone_restricted.check_zone("deep.sub.internal.lan").is_ok());
508    }
509
510    #[rstest]
511    fn zone_check_is_case_insensitive(zone_restricted: Policy) {
512        assert!(zone_restricted.check_zone("EXAMPLE.COM").is_ok());
513        assert!(zone_restricted.check_zone("Sub.Example.Com").is_ok());
514    }
515
516    #[rstest]
517    fn disallowed_zone_is_rejected(zone_restricted: Policy) {
518        let err = zone_restricted.check_zone("other.net").unwrap_err();
519        assert!(matches!(err, Error::PolicyViolation { .. }));
520        assert!(err.to_string().contains("other.net"));
521    }
522
523    #[rstest]
524    fn partial_suffix_without_dot_is_not_allowed(zone_restricted: Policy) {
525        // "notexample.com" must NOT match allowed "example.com"
526        assert!(zone_restricted.check_zone("notexample.com").is_err());
527    }
528
529    // ── instructions_suffix ───────────────────────────────────────────────────
530
531    #[rstest]
532    fn unrestricted_has_no_suffix(unrestricted: Policy) {
533        assert!(unrestricted.instructions_suffix().is_empty());
534    }
535
536    #[rstest]
537    fn readonly_suffix_mentions_read_only(readonly: Policy) {
538        assert!(readonly.instructions_suffix().contains("Read-only"));
539    }
540
541    #[rstest]
542    fn write_access_suffix_mentions_write_mode(write_access: Policy) {
543        assert!(write_access.instructions_suffix().contains("Write mode: delete operations are disabled."));
544    }
545
546    #[rstest]
547    fn write_only_suffix_mentions_write_only(write_only: Policy) {
548        assert!(write_only.instructions_suffix().contains("Write-only"));
549    }
550
551    #[rstest]
552    fn write_delete_suffix_mentions_read_disabled(write_delete: Policy) {
553        assert!(write_delete.instructions_suffix().contains("read operations are disabled"));
554    }
555
556    #[rstest]
557    fn zone_restricted_suffix_mentions_zones(zone_restricted: Policy) {
558        let s = zone_restricted.instructions_suffix();
559        assert!(s.contains("example.com"));
560        assert!(s.contains("internal.lan"));
561    }
562
563    #[rstest]
564    fn both_suffix_mentions_both(both: Policy) {
565        let s = both.instructions_suffix();
566        assert!(s.contains("Read-only"));
567        assert!(s.contains("example.com"));
568    }
569
570    // ── Policy::for_server ────────────────────────────────────────────────────
571
572    use crate::control_plane::config::{DnsServerConfig, McpPermissions, VendorKind};
573
574    /// Constructs a test `DnsServerConfig` with the provided MCP permissions.
575    ///
576    /// The returned config is populated with a fixed id, vendor, token and the given
577    /// `access` and `allowed_zones` embedded in `mcp`. Other fields are left as
578    /// None or empty suitable for unit tests.
579    ///
580    /// # Examples
581    ///
582    /// ```ignore
583    /// let cfg = server_with_mcp(vec![PolicyRule::Read, PolicyRule::Write], vec!["example.com".into()]);
584    /// assert_eq!(cfg.id, "test");
585    /// assert_eq!(cfg.mcp.allowed_zones.len(), 1);
586    /// assert!(cfg.mcp.access.contains(&PolicyRule::Read));
587    /// ```ignore
588    fn server_with_mcp(access: Vec<PolicyRule>, allowed_zones: Vec<String>) -> DnsServerConfig {
589        DnsServerConfig {
590            id: "test".into(),
591            vendor: VendorKind::Technitium,
592            location: None,
593            base_url: None,
594            base_url_env: None,
595            token: Some("tok".into()),
596            token_env: None,
597            org_id: None,
598            cluster: None,
599            dns: None,
600            dot: None,
601            doh: None,
602            mcp: McpPermissions { access, allowed_zones },
603            validation_endpoints: vec![],
604        }
605    }
606
607    #[test]
608    fn for_server_uses_mcp_access_when_cli_access_empty() {
609        let server = server_with_mcp(vec![PolicyRule::Read], vec![]);
610        let policy = Policy::for_server(&server, &[], &[]).unwrap();
611        assert!(policy.check_read().is_ok());
612        assert!(policy.check_write().is_err());
613        assert!(policy.check_delete().is_err());
614    }
615
616    #[test]
617    fn for_server_intersects_cli_access_with_mcp_access() {
618        let server = server_with_mcp(
619            vec![PolicyRule::Read, PolicyRule::Write],
620            vec![],
621        );
622        // CLI requests read+delete but server only allows read+write → intersection is read only
623        let policy =
624            Policy::for_server(&server, &[PolicyRule::Read, PolicyRule::Delete], &[]).unwrap();
625        assert!(policy.check_read().is_ok());
626        assert!(policy.check_write().is_err());
627        assert!(policy.check_delete().is_err());
628    }
629
630    #[test]
631    fn for_server_cli_access_cannot_broaden_mcp_access() {
632        let server = server_with_mcp(vec![PolicyRule::Read], vec![]);
633        // CLI asks for write but server config only permits read → result is still read-only
634        let policy = Policy::for_server(&server, &[PolicyRule::Write], &[]).unwrap();
635        assert!(policy.check_read().is_err());
636        assert!(policy.check_write().is_err());
637    }
638
639    #[test]
640    fn for_server_cli_allow_zone_narrows_mcp_zones() {
641        let server = server_with_mcp(
642            vec![PolicyRule::Read],
643            vec!["example.com".into(), "internal.lan".into()],
644        );
645        let policy =
646            Policy::for_server(&server, &[], &["example.com".to_string()]).unwrap();
647        assert!(policy.check_zone("example.com").is_ok());
648        assert!(policy.check_zone("sub.example.com").is_ok());
649        assert!(policy.check_zone("internal.lan").is_err());
650    }
651
652    #[test]
653    fn for_server_cli_allow_zone_outside_mcp_zones_is_rejected() {
654        let server = server_with_mcp(
655            vec![PolicyRule::Read],
656            vec!["example.com".into()],
657        );
658        let err =
659            Policy::for_server(&server, &[], &["other.net".to_string()]).unwrap_err();
660        assert!(matches!(err, Error::PolicyViolation { .. }));
661        assert!(err.to_string().contains("other.net"));
662    }
663
664    #[test]
665    fn for_server_unrestricted_zones_when_neither_side_configures_them() {
666        let server = server_with_mcp(vec![PolicyRule::Read, PolicyRule::Write, PolicyRule::Delete], vec![]);
667        let policy = Policy::for_server(&server, &[], &[]).unwrap();
668        assert!(policy.allowed_zones.is_none());
669        assert!(policy.check_zone("anything.example.com").is_ok());
670    }
671}