capability.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. //! End-user abstraction for selecting permissions a window has access to.
  5. use std::{path::Path, str::FromStr};
  6. use crate::{acl::Identifier, platform::Target};
  7. use serde::{
  8. de::{Error, IntoDeserializer},
  9. Deserialize, Deserializer, Serialize,
  10. };
  11. use serde_untagged::UntaggedEnumVisitor;
  12. use super::Scopes;
  13. /// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
  14. /// or an object that references a permission and extends its scope.
  15. #[derive(Debug, Clone, PartialEq, Serialize)]
  16. #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
  17. #[serde(untagged)]
  18. pub enum PermissionEntry {
  19. /// Reference a permission or permission set by identifier.
  20. PermissionRef(Identifier),
  21. /// Reference a permission or permission set by identifier and extends its scope.
  22. ExtendedPermission {
  23. /// Identifier of the permission or permission set.
  24. identifier: Identifier,
  25. /// Scope to append to the existing permission scope.
  26. #[serde(default, flatten)]
  27. scope: Scopes,
  28. },
  29. }
  30. impl PermissionEntry {
  31. /// The identifier of the permission referenced in this entry.
  32. pub fn identifier(&self) -> &Identifier {
  33. match self {
  34. Self::PermissionRef(identifier) => identifier,
  35. Self::ExtendedPermission {
  36. identifier,
  37. scope: _,
  38. } => identifier,
  39. }
  40. }
  41. }
  42. impl<'de> Deserialize<'de> for PermissionEntry {
  43. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  44. where
  45. D: Deserializer<'de>,
  46. {
  47. #[derive(Deserialize)]
  48. struct ExtendedPermissionStruct {
  49. identifier: Identifier,
  50. #[serde(default, flatten)]
  51. scope: Scopes,
  52. }
  53. UntaggedEnumVisitor::new()
  54. .string(|string| {
  55. let de = string.into_deserializer();
  56. Identifier::deserialize(de).map(Self::PermissionRef)
  57. })
  58. .map(|map| {
  59. let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
  60. Ok(Self::ExtendedPermission {
  61. identifier: ext_perm.identifier,
  62. scope: ext_perm.scope,
  63. })
  64. })
  65. .deserialize(deserializer)
  66. }
  67. }
  68. /// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
  69. ///
  70. /// It controls application windows fine grained access to the Tauri core, application, or plugin commands.
  71. /// If a window is not matching any capability then it has no access to the IPC layer at all.
  72. ///
  73. /// This can be done to create groups of windows, based on their required system access, which can reduce
  74. /// impact of frontend vulnerabilities in less privileged windows.
  75. /// Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.
  76. /// A Window can have none, one, or multiple associated capabilities.
  77. ///
  78. /// ## Example
  79. ///
  80. /// ```json
  81. /// {
  82. /// "identifier": "main-user-files-write",
  83. /// "description": "This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
  84. /// "windows": [
  85. /// "main"
  86. /// ],
  87. /// "permissions": [
  88. /// "core:default",
  89. /// "dialog:open",
  90. /// {
  91. /// "identifier": "fs:allow-write-text-file",
  92. /// "allow": [{ "path": "$HOME/test.txt" }]
  93. /// },
  94. /// ],
  95. /// "platforms": ["macOS","windows"]
  96. /// }
  97. /// ```
  98. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  99. #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
  100. pub struct Capability {
  101. /// Identifier of the capability.
  102. ///
  103. /// ## Example
  104. ///
  105. /// `main-user-files-write`
  106. ///
  107. pub identifier: String,
  108. /// Description of what the capability is intended to allow on associated windows.
  109. ///
  110. /// It should contain a description of what the grouped permissions should allow.
  111. ///
  112. /// ## Example
  113. ///
  114. /// This capability allows the `main` window access to `filesystem` write related
  115. /// commands and `dialog` commands to enable programatic access to files selected by the user.
  116. #[serde(default)]
  117. pub description: String,
  118. /// Configure remote URLs that can use the capability permissions.
  119. ///
  120. /// This setting is optional and defaults to not being set, as our
  121. /// default use case is that the content is served from our local application.
  122. ///
  123. /// :::caution
  124. /// Make sure you understand the security implications of providing remote
  125. /// sources with local system access.
  126. /// :::
  127. ///
  128. /// ## Example
  129. ///
  130. /// ```json
  131. /// {
  132. /// "urls": ["https://*.mydomain.dev"]
  133. /// }
  134. /// ```
  135. #[serde(default, skip_serializing_if = "Option::is_none")]
  136. pub remote: Option<CapabilityRemote>,
  137. /// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
  138. #[serde(default = "default_capability_local")]
  139. pub local: bool,
  140. /// List of windows that are affected by this capability. Can be a glob pattern.
  141. ///
  142. /// On multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.
  143. ///
  144. /// ## Example
  145. ///
  146. /// `["main"]`
  147. #[serde(default, skip_serializing_if = "Vec::is_empty")]
  148. pub windows: Vec<String>,
  149. /// List of webviews that are affected by this capability. Can be a glob pattern.
  150. ///
  151. /// This is only required when using on multiwebview contexts, by default
  152. /// all child webviews of a window that matches [`Self::windows`] are linked.
  153. ///
  154. /// ## Example
  155. ///
  156. /// `["sub-webview-one", "sub-webview-two"]`
  157. #[serde(default, skip_serializing_if = "Vec::is_empty")]
  158. pub webviews: Vec<String>,
  159. /// List of permissions attached to this capability.
  160. ///
  161. /// Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
  162. /// For commands directly implemented in the application itself only `${permission-name}`
  163. /// is required.
  164. ///
  165. /// ## Example
  166. ///
  167. /// ```json
  168. /// [
  169. /// "core:default",
  170. /// "shell:allow-open",
  171. /// "dialog:open",
  172. /// {
  173. /// "identifier": "fs:allow-write-text-file",
  174. /// "allow": [{ "path": "$HOME/test.txt" }]
  175. /// }
  176. /// ```
  177. #[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
  178. pub permissions: Vec<PermissionEntry>,
  179. /// Limit which target platforms this capability applies to.
  180. ///
  181. /// By default all platforms are targeted.
  182. ///
  183. /// ## Example
  184. ///
  185. /// `["macOS","windows"]`
  186. #[serde(skip_serializing_if = "Option::is_none")]
  187. pub platforms: Option<Vec<Target>>,
  188. }
  189. impl Capability {
  190. /// Whether this capability should be active based on the platform target or not.
  191. pub fn is_active(&self, target: &Target) -> bool {
  192. self
  193. .platforms
  194. .as_ref()
  195. .map(|platforms| platforms.contains(target))
  196. .unwrap_or(true)
  197. }
  198. }
  199. #[cfg(feature = "schema")]
  200. fn unique_permission(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
  201. use schemars::schema;
  202. schema::SchemaObject {
  203. instance_type: Some(schema::InstanceType::Array.into()),
  204. array: Some(Box::new(schema::ArrayValidation {
  205. unique_items: Some(true),
  206. items: Some(gen.subschema_for::<PermissionEntry>().into()),
  207. ..Default::default()
  208. })),
  209. ..Default::default()
  210. }
  211. .into()
  212. }
  213. fn default_capability_local() -> bool {
  214. true
  215. }
  216. /// Configuration for remote URLs that are associated with the capability.
  217. #[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
  218. #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
  219. #[serde(rename_all = "camelCase")]
  220. pub struct CapabilityRemote {
  221. /// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).
  222. ///
  223. /// ## Examples
  224. ///
  225. /// - "https://*.mydomain.dev": allows subdomains of mydomain.dev
  226. /// - "https://mydomain.dev/api/*": allows any subpath of mydomain.dev/api
  227. pub urls: Vec<String>,
  228. }
  229. /// Capability formats accepted in a capability file.
  230. #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
  231. #[cfg_attr(feature = "schema", schemars(untagged))]
  232. #[cfg_attr(test, derive(Debug, PartialEq))]
  233. pub enum CapabilityFile {
  234. /// A single capability.
  235. Capability(Capability),
  236. /// A list of capabilities.
  237. List(Vec<Capability>),
  238. /// A list of capabilities.
  239. NamedList {
  240. /// The list of capabilities.
  241. capabilities: Vec<Capability>,
  242. },
  243. }
  244. impl CapabilityFile {
  245. /// Load the given capability file.
  246. pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
  247. let path = path.as_ref();
  248. let capability_file = std::fs::read_to_string(path).map_err(super::Error::ReadFile)?;
  249. let ext = path.extension().unwrap().to_string_lossy().to_string();
  250. let file: Self = match ext.as_str() {
  251. "toml" => toml::from_str(&capability_file)?,
  252. "json" => serde_json::from_str(&capability_file)?,
  253. _ => return Err(super::Error::UnknownCapabilityFormat(ext)),
  254. };
  255. Ok(file)
  256. }
  257. }
  258. impl<'de> Deserialize<'de> for CapabilityFile {
  259. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  260. where
  261. D: Deserializer<'de>,
  262. {
  263. UntaggedEnumVisitor::new()
  264. .seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
  265. .map(|map| {
  266. #[derive(Deserialize)]
  267. struct CapabilityNamedList {
  268. capabilities: Vec<Capability>,
  269. }
  270. let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
  271. if value.contains_key("capabilities") {
  272. serde_json::from_value::<CapabilityNamedList>(value.into())
  273. .map(|named| Self::NamedList {
  274. capabilities: named.capabilities,
  275. })
  276. .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
  277. } else {
  278. serde_json::from_value::<Capability>(value.into())
  279. .map(Self::Capability)
  280. .map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
  281. }
  282. })
  283. .deserialize(deserializer)
  284. }
  285. }
  286. impl FromStr for CapabilityFile {
  287. type Err = super::Error;
  288. fn from_str(s: &str) -> Result<Self, Self::Err> {
  289. serde_json::from_str(s)
  290. .or_else(|_| toml::from_str(s))
  291. .map_err(Into::into)
  292. }
  293. }
  294. #[cfg(feature = "build")]
  295. mod build {
  296. use std::convert::identity;
  297. use proc_macro2::TokenStream;
  298. use quote::{quote, ToTokens, TokenStreamExt};
  299. use super::*;
  300. use crate::{literal_struct, tokens::*};
  301. impl ToTokens for CapabilityRemote {
  302. fn to_tokens(&self, tokens: &mut TokenStream) {
  303. let urls = vec_lit(&self.urls, str_lit);
  304. literal_struct!(
  305. tokens,
  306. ::tauri::utils::acl::capability::CapabilityRemote,
  307. urls
  308. );
  309. }
  310. }
  311. impl ToTokens for PermissionEntry {
  312. fn to_tokens(&self, tokens: &mut TokenStream) {
  313. let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
  314. tokens.append_all(match self {
  315. Self::PermissionRef(id) => {
  316. quote! { #prefix::PermissionRef(#id) }
  317. }
  318. Self::ExtendedPermission { identifier, scope } => {
  319. quote! { #prefix::ExtendedPermission {
  320. identifier: #identifier,
  321. scope: #scope
  322. } }
  323. }
  324. });
  325. }
  326. }
  327. impl ToTokens for Capability {
  328. fn to_tokens(&self, tokens: &mut TokenStream) {
  329. let identifier = str_lit(&self.identifier);
  330. let description = str_lit(&self.description);
  331. let remote = opt_lit(self.remote.as_ref());
  332. let local = self.local;
  333. let windows = vec_lit(&self.windows, str_lit);
  334. let webviews = vec_lit(&self.webviews, str_lit);
  335. let permissions = vec_lit(&self.permissions, identity);
  336. let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
  337. literal_struct!(
  338. tokens,
  339. ::tauri::utils::acl::capability::Capability,
  340. identifier,
  341. description,
  342. remote,
  343. local,
  344. windows,
  345. webviews,
  346. permissions,
  347. platforms
  348. );
  349. }
  350. }
  351. }
  352. #[cfg(test)]
  353. mod tests {
  354. use crate::acl::{Identifier, Scopes};
  355. use super::{Capability, CapabilityFile, PermissionEntry};
  356. #[test]
  357. fn permission_entry_de() {
  358. let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
  359. let identifier_json = serde_json::to_string(&identifier).unwrap();
  360. assert_eq!(
  361. serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
  362. PermissionEntry::PermissionRef(identifier.clone())
  363. );
  364. assert_eq!(
  365. serde_json::from_value::<PermissionEntry>(serde_json::json!({
  366. "identifier": identifier,
  367. "allow": [],
  368. "deny": null
  369. }))
  370. .unwrap(),
  371. PermissionEntry::ExtendedPermission {
  372. identifier,
  373. scope: Scopes {
  374. allow: Some(vec![]),
  375. deny: None
  376. }
  377. }
  378. );
  379. }
  380. #[test]
  381. fn capability_file_de() {
  382. let capability = Capability {
  383. identifier: "test".into(),
  384. description: "".into(),
  385. remote: None,
  386. local: true,
  387. windows: vec![],
  388. webviews: vec![],
  389. permissions: vec![],
  390. platforms: None,
  391. };
  392. let capability_json = serde_json::to_string(&capability).unwrap();
  393. assert_eq!(
  394. serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
  395. CapabilityFile::Capability(capability.clone())
  396. );
  397. assert_eq!(
  398. serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
  399. CapabilityFile::List(vec![capability.clone()])
  400. );
  401. assert_eq!(
  402. serde_json::from_str::<CapabilityFile>(&format!(
  403. "{{ \"capabilities\": [{capability_json}] }}"
  404. ))
  405. .unwrap(),
  406. CapabilityFile::NamedList {
  407. capabilities: vec![capability.clone()]
  408. }
  409. );
  410. }
  411. }