Skip to main content

origin_manifest/
security.rs

1use crate::{Manifest, SecurityProfile};
2use serde::Serialize;
3use std::collections::BTreeMap;
4
5/// A Tauri capability file, as generated from the manifest.
6#[derive(Debug, Clone, Serialize)]
7pub struct Capability {
8    #[serde(rename = "$schema")]
9    pub schema: String,
10    pub identifier: String,
11    pub description: String,
12    pub windows: Vec<String>,
13    pub permissions: Vec<String>,
14}
15
16impl Capability {
17    /// One capability file per profile, listing the windows that use it.
18    ///
19    /// Grouped by profile rather than per window: two windows with the same profile
20    /// should visibly share one grant, so widening it is one obvious diff instead of
21    /// two that can drift apart.
22    pub fn from_manifest(manifest: &Manifest) -> Vec<Self> {
23        let mut by_profile: BTreeMap<SecurityProfile, Vec<String>> = BTreeMap::new();
24
25        for (window, security) in &manifest.security.windows {
26            by_profile
27                .entry(security.profile)
28                .or_default()
29                .push(window.clone());
30        }
31
32        by_profile
33            .into_iter()
34            .map(|(profile, mut windows)| {
35                windows.sort();
36                Self {
37                    schema: "../gen/schemas/desktop-schema.json".to_owned(),
38                    identifier: profile.identifier().to_owned(),
39                    description: format!(
40                        "{} Generated from app.toml — do not edit.",
41                        profile.description()
42                    ),
43                    windows,
44                    permissions: profile
45                        .permissions()
46                        .iter()
47                        .map(|permission| (*permission).to_owned())
48                        .collect(),
49                }
50            })
51            .collect()
52    }
53
54    /// File name this capability is written to.
55    pub fn file_name(&self) -> String {
56        format!("{}.json", self.identifier)
57    }
58}
59
60// `SecurityProfile` is used as a map key above, so it needs a total order.
61impl PartialOrd for SecurityProfile {
62    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
63        Some(self.cmp(other))
64    }
65}
66
67impl Ord for SecurityProfile {
68    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
69        self.identifier().cmp(other.identifier())
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn manifest(windows: &str) -> Manifest {
78        let contents = format!(
79            r#"
80[origin]
81version = "0.1.0"
82
83[product]
84id = "dev.origin.demo"
85name = "Demo"
86version = "0.1.0"
87{windows}
88"#
89        );
90        toml::from_str(&contents).unwrap()
91    }
92
93    #[test]
94    fn windows_sharing_a_profile_share_one_capability_file() {
95        let manifest = manifest(
96            "\n[security.windows.main]\nprofile = \"standard-dashboard\"\n\
97             \n[security.windows.detail]\nprofile = \"standard-dashboard\"\n",
98        );
99
100        let capabilities = Capability::from_manifest(&manifest);
101
102        assert_eq!(capabilities.len(), 1);
103        assert_eq!(capabilities[0].windows, vec!["detail", "main"]);
104    }
105
106    #[test]
107    fn different_profiles_produce_separate_files() {
108        let manifest = manifest(
109            "\n[security.windows.main]\nprofile = \"standard-dashboard\"\n\
110             \n[security.windows.settings]\nprofile = \"account-settings\"\n",
111        );
112
113        let capabilities = Capability::from_manifest(&manifest);
114
115        assert_eq!(capabilities.len(), 2);
116        assert_eq!(capabilities[0].file_name(), "account-settings.json");
117        assert_eq!(capabilities[1].file_name(), "standard-dashboard.json");
118    }
119
120    #[test]
121    fn the_generated_file_says_it_is_generated() {
122        let manifest = manifest("\n[security.windows.main]\nprofile = \"readonly-dashboard\"\n");
123
124        let capability = &Capability::from_manifest(&manifest)[0];
125
126        assert!(
127            capability.description.contains("do not edit"),
128            "someone opening this file must see where it comes from"
129        );
130    }
131
132    #[test]
133    fn a_multi_window_product_gets_one_capability_per_profile() {
134        // B8: the window set a local-resource product needs — a main window, settings,
135        // a quick-capture overlay and a workspace window — each with its own profile.
136        let manifest = manifest(
137            "\n[security.windows.main]\nprofile = \"standard-dashboard\"\n\
138             \n[security.windows.settings]\nprofile = \"account-settings\"\n\
139             \n[security.windows.quick_capture]\nprofile = \"readonly-dashboard\"\n\
140             \n[security.windows.workspace]\nprofile = \"local-workspace\"\n",
141        );
142
143        let capabilities = Capability::from_manifest(&manifest);
144        let files: Vec<String> = capabilities.iter().map(Capability::file_name).collect();
145
146        assert_eq!(
147            files,
148            vec![
149                "account-settings.json",
150                "local-workspace.json",
151                "readonly-dashboard.json",
152                "standard-dashboard.json",
153            ]
154        );
155
156        let windows_of = |identifier: &str| {
157            capabilities
158                .iter()
159                .find(|capability| capability.identifier == identifier)
160                .map(|capability| capability.windows.clone())
161                .unwrap_or_default()
162        };
163
164        assert_eq!(windows_of("standard-dashboard"), vec!["main"]);
165        assert_eq!(windows_of("account-settings"), vec!["settings"]);
166        assert_eq!(windows_of("readonly-dashboard"), vec!["quick_capture"]);
167        assert_eq!(windows_of("local-workspace"), vec!["workspace"]);
168    }
169}