acl.rs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // Copyright 2019-2023 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},
  6. fs::{copy, create_dir_all, read_to_string, write},
  7. path::PathBuf,
  8. };
  9. use anyhow::{Context, Result};
  10. use schemars::{
  11. schema::{
  12. ArrayValidation, InstanceType, Metadata, ObjectValidation, RootSchema, Schema, SchemaObject,
  13. SubschemaValidation,
  14. },
  15. schema_for,
  16. };
  17. use tauri_utils::{
  18. acl::{build::CapabilityFile, capability::Capability, plugin::Manifest},
  19. platform::Target,
  20. };
  21. const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
  22. /// Path of the folder where schemas are saved.
  23. const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "capabilities/schemas";
  24. const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
  25. const PLUGIN_MANIFESTS_FILE_NAME: &str = "plugin-manifests.json";
  26. fn capabilities_schema(plugin_manifests: &BTreeMap<String, Manifest>) -> RootSchema {
  27. let mut schema = schema_for!(CapabilityFile);
  28. fn schema_from(plugin: &str, id: &str, description: Option<&str>) -> Schema {
  29. Schema::Object(SchemaObject {
  30. metadata: Some(Box::new(Metadata {
  31. description: description
  32. .as_ref()
  33. .map(|d| format!("{plugin}:{id} -> {d}")),
  34. ..Default::default()
  35. })),
  36. instance_type: Some(InstanceType::String.into()),
  37. enum_values: Some(vec![serde_json::Value::String(format!("{plugin}:{id}"))]),
  38. ..Default::default()
  39. })
  40. }
  41. let mut permission_schemas = Vec::new();
  42. for (plugin, manifest) in plugin_manifests {
  43. for (set_id, set) in &manifest.permission_sets {
  44. permission_schemas.push(schema_from(plugin, set_id, Some(&set.description)));
  45. }
  46. if let Some(default) = &manifest.default_permission {
  47. permission_schemas.push(schema_from(
  48. plugin,
  49. "default",
  50. Some(default.description.as_ref()),
  51. ));
  52. }
  53. for (permission_id, permission) in &manifest.permissions {
  54. permission_schemas.push(schema_from(
  55. plugin,
  56. permission_id,
  57. permission.description.as_deref(),
  58. ));
  59. }
  60. }
  61. if let Some(Schema::Object(obj)) = schema.definitions.get_mut("Identifier") {
  62. obj.object = None;
  63. obj.instance_type = None;
  64. obj.metadata.as_mut().map(|metadata| {
  65. metadata
  66. .description
  67. .replace("Permission identifier".to_string());
  68. metadata
  69. });
  70. obj.subschemas.replace(Box::new(SubschemaValidation {
  71. one_of: Some(permission_schemas),
  72. ..Default::default()
  73. }));
  74. }
  75. if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionEntry") {
  76. let permission_entry_any_of_schemas = obj.subschemas().any_of.as_mut().unwrap();
  77. if let Schema::Object(mut scope_extended_schema_obj) =
  78. permission_entry_any_of_schemas.remove(permission_entry_any_of_schemas.len() - 1)
  79. {
  80. let mut global_scope_one_of = Vec::new();
  81. for (plugin, manifest) in plugin_manifests {
  82. if let Some(global_scope_schema) = &manifest.global_scope_schema {
  83. let global_scope_schema_def: Schema = serde_json::from_value(global_scope_schema.clone())
  84. .unwrap_or_else(|e| panic!("invalid JSON schema for plugin {plugin}: {e}"));
  85. let global_scope_schema = Schema::Object(SchemaObject {
  86. array: Some(Box::new(ArrayValidation {
  87. items: Some(global_scope_schema_def.into()),
  88. ..Default::default()
  89. })),
  90. ..Default::default()
  91. });
  92. let mut required = BTreeSet::new();
  93. required.insert("identifier".to_string());
  94. let mut object = ObjectValidation {
  95. required,
  96. ..Default::default()
  97. };
  98. let mut permission_schemas = Vec::new();
  99. if let Some(default) = &manifest.default_permission {
  100. permission_schemas.push(schema_from(plugin, "default", Some(&default.description)));
  101. }
  102. for set in manifest.permission_sets.values() {
  103. permission_schemas.push(schema_from(plugin, &set.identifier, Some(&set.description)));
  104. }
  105. for permission in manifest.permissions.values() {
  106. permission_schemas.push(schema_from(
  107. plugin,
  108. &permission.identifier,
  109. permission.description.as_deref(),
  110. ));
  111. }
  112. let identifier_schema = Schema::Object(SchemaObject {
  113. subschemas: Some(Box::new(SubschemaValidation {
  114. one_of: Some(permission_schemas),
  115. ..Default::default()
  116. })),
  117. ..Default::default()
  118. });
  119. object
  120. .properties
  121. .insert("identifier".to_string(), identifier_schema);
  122. object
  123. .properties
  124. .insert("allow".to_string(), global_scope_schema.clone());
  125. object
  126. .properties
  127. .insert("deny".to_string(), global_scope_schema);
  128. global_scope_one_of.push(Schema::Object(SchemaObject {
  129. instance_type: Some(InstanceType::Object.into()),
  130. object: Some(Box::new(object)),
  131. ..Default::default()
  132. }));
  133. }
  134. }
  135. if !global_scope_one_of.is_empty() {
  136. scope_extended_schema_obj.object = None;
  137. scope_extended_schema_obj
  138. .subschemas
  139. .replace(Box::new(SubschemaValidation {
  140. one_of: Some(global_scope_one_of),
  141. ..Default::default()
  142. }));
  143. permission_entry_any_of_schemas.push(scope_extended_schema_obj.into());
  144. };
  145. }
  146. }
  147. schema
  148. }
  149. pub fn generate_schema(
  150. plugin_manifests: &BTreeMap<String, Manifest>,
  151. target: Target,
  152. ) -> Result<()> {
  153. let schema = capabilities_schema(plugin_manifests);
  154. let schema_str = serde_json::to_string_pretty(&schema).unwrap();
  155. let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
  156. create_dir_all(&out_dir).context("unable to create schema output directory")?;
  157. let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
  158. if schema_str != read_to_string(&schema_path).unwrap_or_default() {
  159. write(&schema_path, "{schema_str}")?;
  160. copy(
  161. schema_path,
  162. out_dir.join(format!(
  163. "{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
  164. if target.is_desktop() {
  165. "desktop"
  166. } else {
  167. "mobile"
  168. }
  169. )),
  170. )?;
  171. }
  172. Ok(())
  173. }
  174. pub fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
  175. let capabilities_path =
  176. PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(CAPABILITIES_FILE_NAME);
  177. let capabilities_json = serde_json::to_string(&capabilities)?;
  178. if capabilities_json != read_to_string(&capabilities_path).unwrap_or_default() {
  179. std::fs::write(&capabilities_path, capabilities_json)?;
  180. }
  181. Ok(capabilities_path)
  182. }
  183. pub fn save_plugin_manifests(plugin_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
  184. let plugin_manifests_path =
  185. PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH).join(PLUGIN_MANIFESTS_FILE_NAME);
  186. let plugin_manifests_json = serde_json::to_string(&plugin_manifests)?;
  187. if plugin_manifests_json != read_to_string(&plugin_manifests_path).unwrap_or_default() {
  188. std::fs::write(&plugin_manifests_path, plugin_manifests_json)?;
  189. }
  190. Ok(plugin_manifests_path)
  191. }
  192. pub fn get_plugin_manifests() -> Result<BTreeMap<String, Manifest>> {
  193. let permission_map =
  194. tauri_utils::acl::build::read_permissions().context("failed to read plugin permissions")?;
  195. let mut global_scope_map = tauri_utils::acl::build::read_global_scope_schemas()
  196. .context("failed to read global scope schemas")?;
  197. let mut processed = BTreeMap::new();
  198. for (plugin_name, permission_files) in permission_map {
  199. let manifest = Manifest::new(permission_files, global_scope_map.remove(&plugin_name));
  200. processed.insert(plugin_name, manifest);
  201. }
  202. Ok(processed)
  203. }
  204. pub fn validate_capabilities(
  205. plugin_manifests: &BTreeMap<String, Manifest>,
  206. capabilities: &BTreeMap<String, Capability>,
  207. ) -> Result<()> {
  208. let target = tauri_utils::platform::Target::from_triple(&std::env::var("TARGET").unwrap());
  209. for capability in capabilities.values() {
  210. if !capability.platforms.contains(&target) {
  211. continue;
  212. }
  213. for permission_entry in &capability.permissions {
  214. let permission_id = permission_entry.identifier();
  215. if let Some((plugin_name, permission_name)) = permission_id.get().split_once(':') {
  216. let permission_exists = plugin_manifests
  217. .get(plugin_name)
  218. .map(|manifest| {
  219. if permission_name == "default" {
  220. manifest.default_permission.is_some()
  221. } else {
  222. manifest.permissions.contains_key(permission_name)
  223. || manifest.permission_sets.contains_key(permission_name)
  224. }
  225. })
  226. .unwrap_or(false);
  227. if !permission_exists {
  228. let mut available_permissions = Vec::new();
  229. for (plugin, manifest) in plugin_manifests {
  230. if manifest.default_permission.is_some() {
  231. available_permissions.push(format!("{plugin}:default"));
  232. }
  233. for p in manifest.permissions.keys() {
  234. available_permissions.push(format!("{plugin}:{p}"));
  235. }
  236. for p in manifest.permission_sets.keys() {
  237. available_permissions.push(format!("{plugin}:{p}"));
  238. }
  239. }
  240. anyhow::bail!(
  241. "Permission {} not found, expected one of {}",
  242. permission_id.get(),
  243. available_permissions.join(", ")
  244. );
  245. }
  246. }
  247. }
  248. }
  249. Ok(())
  250. }