acl.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. collections::{BTreeMap, BTreeSet, HashMap},
  6. env::current_dir,
  7. fs::{copy, create_dir_all, read_to_string, write},
  8. path::{Path, PathBuf},
  9. };
  10. use anyhow::{Context, Result};
  11. use schemars::{
  12. schema::{
  13. ArrayValidation, InstanceType, Metadata, ObjectValidation, RootSchema, Schema, SchemaObject,
  14. SubschemaValidation,
  15. },
  16. schema_for,
  17. };
  18. use tauri_utils::{
  19. acl::{
  20. capability::{Capability, CapabilityFile},
  21. manifest::Manifest,
  22. APP_ACL_KEY,
  23. },
  24. platform::Target,
  25. };
  26. const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
  27. /// Path of the folder where schemas are saved.
  28. const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
  29. const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
  30. const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
  31. /// Definition of a plugin that is part of the Tauri application instead of having its own crate.
  32. ///
  33. /// By default it generates a plugin manifest that parses permissions from the `permissions/$plugin-name` directory.
  34. /// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`].
  35. ///
  36. /// To autogenerate permissions for each of the plugin commands, see [`Self::commands`].
  37. #[derive(Debug, Default)]
  38. pub struct InlinedPlugin {
  39. commands: &'static [&'static str],
  40. permissions_path_pattern: Option<&'static str>,
  41. }
  42. impl InlinedPlugin {
  43. pub fn new() -> Self {
  44. Self::default()
  45. }
  46. /// Define a list of commands that gets permissions autogenerated in the format of `allow-$command` and `deny-$command`
  47. /// where $command is the command name in snake_case.
  48. pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
  49. self.commands = commands;
  50. self
  51. }
  52. /// Sets a glob pattern that is used to find the permissions of this inlined plugin.
  53. ///
  54. /// **Note:** You must emit [rerun-if-changed] instructions for the plugin permissions directory.
  55. ///
  56. /// By default it is `./permissions/$plugin-name/**/*`
  57. pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
  58. self.permissions_path_pattern.replace(pattern);
  59. self
  60. }
  61. }
  62. /// Tauri application permission manifest.
  63. ///
  64. /// By default it generates a manifest that parses permissions from the `permissions` directory.
  65. /// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`].
  66. ///
  67. /// To autogenerate permissions for each of the app commands, see [`Self::commands`].
  68. #[derive(Debug, Default)]
  69. pub struct AppManifest {
  70. commands: &'static [&'static str],
  71. permissions_path_pattern: Option<&'static str>,
  72. }
  73. impl AppManifest {
  74. pub fn new() -> Self {
  75. Self::default()
  76. }
  77. /// Define a list of commands that gets permissions autogenerated in the format of `allow-$command` and `deny-$command`
  78. /// where $command is the command name in snake_case.
  79. pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
  80. self.commands = commands;
  81. self
  82. }
  83. /// Sets a glob pattern that is used to find the permissions of the app.
  84. ///
  85. /// **Note:** You must emit [rerun-if-changed] instructions for the permissions directory.
  86. ///
  87. /// By default it is `./permissions/**/*` ignoring any [`InlinedPlugin`].
  88. pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
  89. self.permissions_path_pattern.replace(pattern);
  90. self
  91. }
  92. }
  93. fn capabilities_schema(acl_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
  94. let mut schema = schema_for!(CapabilityFile);
  95. fn schema_from(key: &str, id: &str, description: Option<&str>) -> Schema {
  96. let command_name = if key == APP_ACL_KEY {
  97. id.to_string()
  98. } else {
  99. format!("{key}:{id}")
  100. };
  101. Schema::Object(SchemaObject {
  102. metadata: Some(Box::new(Metadata {
  103. description: description
  104. .as_ref()
  105. .map(|d| format!("{command_name} -> {d}")),
  106. ..Default::default()
  107. })),
  108. instance_type: Some(InstanceType::String.into()),
  109. enum_values: Some(vec![serde_json::Value::String(command_name)]),
  110. ..Default::default()
  111. })
  112. }
  113. let mut permission_schemas = Vec::new();
  114. for (key, manifest) in acl_manifests {
  115. for (set_id, set) in &manifest.permission_sets {
  116. permission_schemas.push(schema_from(key, set_id, Some(&set.description)));
  117. }
  118. permission_schemas.push(schema_from(
  119. key,
  120. "default",
  121. manifest
  122. .default_permission
  123. .as_ref()
  124. .map(|d| d.description.as_ref()),
  125. ));
  126. for (permission_id, permission) in &manifest.permissions {
  127. permission_schemas.push(schema_from(
  128. key,
  129. permission_id,
  130. permission.description.as_deref(),
  131. ));
  132. }
  133. }
  134. if let Some(Schema::Object(obj)) = schema.definitions.get_mut("Identifier") {
  135. obj.object = None;
  136. obj.instance_type = None;
  137. obj.metadata.as_mut().map(|metadata| {
  138. metadata
  139. .description
  140. .replace("Permission identifier".to_string());
  141. metadata
  142. });
  143. obj.subschemas.replace(Box::new(SubschemaValidation {
  144. one_of: Some(permission_schemas),
  145. ..Default::default()
  146. }));
  147. }
  148. let mut definitions = Vec::new();
  149. if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionEntry") {
  150. let permission_entry_any_of_schemas = obj.subschemas().any_of.as_mut().unwrap();
  151. if let Schema::Object(scope_extended_schema_obj) =
  152. permission_entry_any_of_schemas.last_mut().unwrap()
  153. {
  154. let mut global_scope_one_of = Vec::new();
  155. for (key, manifest) in acl_manifests {
  156. if let Some(global_scope_schema) = &manifest.global_scope_schema {
  157. let global_scope_schema_def: RootSchema =
  158. serde_json::from_value(global_scope_schema.clone())
  159. .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {key}: {e}"));
  160. let global_scope_schema = Schema::Object(SchemaObject {
  161. array: Some(Box::new(ArrayValidation {
  162. items: Some(Schema::Object(global_scope_schema_def.schema).into()),
  163. ..Default::default()
  164. })),
  165. ..Default::default()
  166. });
  167. definitions.push(global_scope_schema_def.definitions);
  168. let mut required = BTreeSet::new();
  169. required.insert("identifier".to_string());
  170. let mut object = ObjectValidation {
  171. required,
  172. ..Default::default()
  173. };
  174. let mut permission_schemas = Vec::new();
  175. permission_schemas.push(schema_from(
  176. key,
  177. "default",
  178. manifest
  179. .default_permission
  180. .as_ref()
  181. .map(|d| d.description.as_ref()),
  182. ));
  183. for set in manifest.permission_sets.values() {
  184. permission_schemas.push(schema_from(key, &set.identifier, Some(&set.description)));
  185. }
  186. for permission in manifest.permissions.values() {
  187. permission_schemas.push(schema_from(
  188. key,
  189. &permission.identifier,
  190. permission.description.as_deref(),
  191. ));
  192. }
  193. let identifier_schema = Schema::Object(SchemaObject {
  194. subschemas: Some(Box::new(SubschemaValidation {
  195. one_of: Some(permission_schemas),
  196. ..Default::default()
  197. })),
  198. ..Default::default()
  199. });
  200. object
  201. .properties
  202. .insert("identifier".to_string(), identifier_schema);
  203. object
  204. .properties
  205. .insert("allow".to_string(), global_scope_schema.clone());
  206. object
  207. .properties
  208. .insert("deny".to_string(), global_scope_schema);
  209. global_scope_one_of.push(Schema::Object(SchemaObject {
  210. instance_type: Some(InstanceType::Object.into()),
  211. object: Some(Box::new(object)),
  212. ..Default::default()
  213. }));
  214. }
  215. }
  216. if !global_scope_one_of.is_empty() {
  217. scope_extended_schema_obj.object = None;
  218. scope_extended_schema_obj
  219. .subschemas
  220. .replace(Box::new(SubschemaValidation {
  221. one_of: Some(global_scope_one_of),
  222. ..Default::default()
  223. }));
  224. };
  225. }
  226. }
  227. for definitions_map in definitions {
  228. schema.definitions.extend(definitions_map);
  229. }
  230. schema
  231. }
  232. pub fn generate_schema(acl_manifests: &BTreeMap<String, Manifest>, target: Target) -> Result<()> {
  233. let schema = capabilities_schema(acl_manifests);
  234. let schema_str = serde_json::to_string_pretty(&schema).unwrap();
  235. let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
  236. create_dir_all(&out_dir).context("unable to create schema output directory")?;
  237. let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
  238. if schema_str != read_to_string(&schema_path).unwrap_or_default() {
  239. write(&schema_path, schema_str)?;
  240. copy(
  241. schema_path,
  242. out_dir.join(format!(
  243. "{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
  244. if target.is_desktop() {
  245. "desktop"
  246. } else {
  247. "mobile"
  248. }
  249. )),
  250. )?;
  251. }
  252. Ok(())
  253. }
  254. pub fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
  255. let capabilities_path =
  256. PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(CAPABILITIES_FILE_NAME);
  257. let capabilities_json = serde_json::to_string(&capabilities)?;
  258. if capabilities_json != read_to_string(&capabilities_path).unwrap_or_default() {
  259. std::fs::write(&capabilities_path, capabilities_json)?;
  260. }
  261. Ok(capabilities_path)
  262. }
  263. pub fn save_acl_manifests(acl_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
  264. let acl_manifests_path =
  265. PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(ACL_MANIFESTS_FILE_NAME);
  266. let acl_manifests_json = serde_json::to_string(&acl_manifests)?;
  267. if acl_manifests_json != read_to_string(&acl_manifests_path).unwrap_or_default() {
  268. std::fs::write(&acl_manifests_path, acl_manifests_json)?;
  269. }
  270. Ok(acl_manifests_path)
  271. }
  272. pub fn get_manifests_from_plugins() -> Result<BTreeMap<String, Manifest>> {
  273. let permission_map =
  274. tauri_utils::acl::build::read_permissions().context("failed to read plugin permissions")?;
  275. let mut global_scope_map = tauri_utils::acl::build::read_global_scope_schemas()
  276. .context("failed to read global scope schemas")?;
  277. let mut processed = BTreeMap::new();
  278. for (plugin_name, permission_files) in permission_map {
  279. let manifest = Manifest::new(permission_files, global_scope_map.remove(&plugin_name));
  280. processed.insert(plugin_name, manifest);
  281. }
  282. Ok(processed)
  283. }
  284. pub fn inline_plugins(
  285. out_dir: &Path,
  286. inlined_plugins: HashMap<&'static str, InlinedPlugin>,
  287. ) -> Result<BTreeMap<String, Manifest>> {
  288. let mut acl_manifests = BTreeMap::new();
  289. for (name, plugin) in inlined_plugins {
  290. let plugin_out_dir = out_dir.join("plugins").join(name);
  291. create_dir_all(&plugin_out_dir)?;
  292. let mut permission_files = if plugin.commands.is_empty() {
  293. Vec::new()
  294. } else {
  295. tauri_utils::acl::build::autogenerate_command_permissions(
  296. &plugin_out_dir,
  297. plugin.commands,
  298. "",
  299. false,
  300. );
  301. tauri_utils::acl::build::define_permissions(
  302. &plugin_out_dir.join("*").to_string_lossy(),
  303. name,
  304. &plugin_out_dir,
  305. |_| true,
  306. )?
  307. };
  308. if let Some(pattern) = plugin.permissions_path_pattern {
  309. permission_files.extend(tauri_utils::acl::build::define_permissions(
  310. pattern,
  311. name,
  312. &plugin_out_dir,
  313. |_| true,
  314. )?);
  315. } else {
  316. let default_permissions_path = Path::new("permissions").join(name);
  317. println!(
  318. "cargo:rerun-if-changed={}",
  319. default_permissions_path.display()
  320. );
  321. permission_files.extend(tauri_utils::acl::build::define_permissions(
  322. &default_permissions_path
  323. .join("**")
  324. .join("*")
  325. .to_string_lossy(),
  326. name,
  327. &plugin_out_dir,
  328. |_| true,
  329. )?);
  330. }
  331. let manifest = tauri_utils::acl::manifest::Manifest::new(permission_files, None);
  332. acl_manifests.insert(name.into(), manifest);
  333. }
  334. Ok(acl_manifests)
  335. }
  336. pub fn app_manifest_permissions(
  337. out_dir: &Path,
  338. manifest: AppManifest,
  339. inlined_plugins: &HashMap<&'static str, InlinedPlugin>,
  340. ) -> Result<Manifest> {
  341. let app_out_dir = out_dir.join("app-manifest");
  342. create_dir_all(&app_out_dir)?;
  343. let pkg_name = "__app__";
  344. let mut permission_files = if manifest.commands.is_empty() {
  345. Vec::new()
  346. } else {
  347. let autogenerated_path = Path::new("./permissions/autogenerated");
  348. tauri_utils::acl::build::autogenerate_command_permissions(
  349. autogenerated_path,
  350. manifest.commands,
  351. "",
  352. false,
  353. );
  354. tauri_utils::acl::build::define_permissions(
  355. &autogenerated_path.join("*").to_string_lossy(),
  356. pkg_name,
  357. &app_out_dir,
  358. |_| true,
  359. )?
  360. };
  361. if let Some(pattern) = manifest.permissions_path_pattern {
  362. permission_files.extend(tauri_utils::acl::build::define_permissions(
  363. pattern,
  364. pkg_name,
  365. &app_out_dir,
  366. |_| true,
  367. )?);
  368. } else {
  369. let default_permissions_path = Path::new("permissions");
  370. println!(
  371. "cargo:rerun-if-changed={}",
  372. default_permissions_path.display()
  373. );
  374. let permissions_root = current_dir()?.join("permissions");
  375. let inlined_plugins_permissions: Vec<_> = inlined_plugins
  376. .keys()
  377. .map(|name| permissions_root.join(name))
  378. .collect();
  379. permission_files.extend(tauri_utils::acl::build::define_permissions(
  380. &default_permissions_path
  381. .join("**")
  382. .join("*")
  383. .to_string_lossy(),
  384. pkg_name,
  385. &app_out_dir,
  386. // filter out directories containing inlined plugins
  387. |p| {
  388. !inlined_plugins_permissions
  389. .iter()
  390. .any(|inlined_path| p.starts_with(inlined_path))
  391. },
  392. )?);
  393. }
  394. Ok(tauri_utils::acl::manifest::Manifest::new(
  395. permission_files,
  396. None,
  397. ))
  398. }
  399. pub fn validate_capabilities(
  400. acl_manifests: &BTreeMap<String, Manifest>,
  401. capabilities: &BTreeMap<String, Capability>,
  402. ) -> Result<()> {
  403. let target = tauri_utils::platform::Target::from_triple(&std::env::var("TARGET").unwrap());
  404. for capability in capabilities.values() {
  405. if !capability
  406. .platforms
  407. .as_ref()
  408. .map(|platforms| platforms.contains(&target))
  409. .unwrap_or(true)
  410. {
  411. continue;
  412. }
  413. for permission_entry in &capability.permissions {
  414. let permission_id = permission_entry.identifier();
  415. let (key, permission_name) = permission_id
  416. .get()
  417. .split_once(':')
  418. .unwrap_or_else(|| (APP_ACL_KEY, permission_id.get()));
  419. let permission_exists = acl_manifests
  420. .get(key)
  421. .map(|manifest| {
  422. // the default permission is always treated as valid, the CLI automatically adds it on the `tauri add` command
  423. permission_name == "default"
  424. || manifest.permissions.contains_key(permission_name)
  425. || manifest.permission_sets.contains_key(permission_name)
  426. })
  427. .unwrap_or(false);
  428. if !permission_exists {
  429. let mut available_permissions = Vec::new();
  430. for (key, manifest) in acl_manifests {
  431. let prefix = if key == APP_ACL_KEY {
  432. "".to_string()
  433. } else {
  434. format!("{key}:")
  435. };
  436. if manifest.default_permission.is_some() {
  437. available_permissions.push(format!("{prefix}default"));
  438. }
  439. for p in manifest.permissions.keys() {
  440. available_permissions.push(format!("{prefix}{p}"));
  441. }
  442. for p in manifest.permission_sets.keys() {
  443. available_permissions.push(format!("{prefix}{p}"));
  444. }
  445. }
  446. anyhow::bail!(
  447. "Permission {} not found, expected one of {}",
  448. permission_id.get(),
  449. available_permissions.join(", ")
  450. );
  451. }
  452. }
  453. }
  454. Ok(())
  455. }