1use std::{
6 collections::{BTreeMap, HashMap},
7 env, fs,
8 path::{Path, PathBuf},
9};
10
11use anyhow::{Context, Result};
12use tauri_utils::{
13 acl::{
14 ACL_MANIFESTS_FILE_NAME, APP_ACL_KEY, CAPABILITIES_FILE_NAME,
15 capability::Capability,
16 get_capabilities,
17 manifest::{Manifest, PermissionFile},
18 schema::CAPABILITIES_SCHEMA_FOLDER_PATH,
19 },
20 config::Config,
21 platform::Target,
22 write_if_changed,
23};
24
25use crate::Attributes;
26
27#[derive(Debug, Default, Clone)]
34pub struct InlinedPlugin {
35 commands: &'static [&'static str],
36 permissions_path_pattern: Option<&'static str>,
37 default: Option<DefaultPermissionRule>,
38}
39
40#[derive(Debug, Clone)]
42pub enum DefaultPermissionRule {
43 AllowAllCommands,
45 Allow(Vec<String>),
50}
51
52impl InlinedPlugin {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
61 self.commands = commands;
62 self
63 }
64
65 pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
71 self.permissions_path_pattern.replace(pattern);
72 self
73 }
74
75 pub fn default_permission(mut self, default: DefaultPermissionRule) -> Self {
79 self.default.replace(default);
80 self
81 }
82}
83
84#[derive(Debug, Default, Clone, Copy)]
91pub struct AppManifest {
92 commands: &'static [&'static str],
93 permissions_path_pattern: Option<&'static str>,
94}
95
96impl AppManifest {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
105 self.commands = commands;
106 self
107 }
108
109 pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
115 self.permissions_path_pattern.replace(pattern);
116 self
117 }
118}
119
120fn save_capabilities(capabilities: &BTreeMap<String, Capability>) -> Result<PathBuf> {
122 let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH);
123 fs::create_dir_all(dir)?;
124
125 let path = dir.join(CAPABILITIES_FILE_NAME);
126 let json = serde_json::to_string(&capabilities)?;
127 write_if_changed(&path, json)?;
128
129 Ok(path)
130}
131
132fn save_acl_manifests(acl_manifests: &BTreeMap<String, Manifest>) -> Result<PathBuf> {
134 let dir = Path::new(CAPABILITIES_SCHEMA_FOLDER_PATH);
135 fs::create_dir_all(dir)?;
136
137 let path = dir.join(ACL_MANIFESTS_FILE_NAME);
138 let json = serde_json::to_string(&acl_manifests)?;
139 write_if_changed(&path, json)?;
140
141 Ok(path)
142}
143
144fn read_plugins_manifests() -> Result<BTreeMap<String, Manifest>> {
146 use tauri_utils::acl;
147
148 let permission_map =
149 acl::build::read_permissions().context("failed to read plugin permissions")?;
150 let mut global_scope_map =
151 acl::build::read_global_scope_schemas().context("failed to read global scope schemas")?;
152
153 let mut manifests = BTreeMap::new();
154
155 for (plugin_name, permission_files) in permission_map {
156 let global_scope_schema = global_scope_map.remove(&plugin_name);
157 let manifest = Manifest::new(permission_files, global_scope_schema);
158 manifests.insert(plugin_name, manifest);
159 }
160
161 Ok(manifests)
162}
163
164struct InlinedPluginsAcl {
165 manifests: BTreeMap<String, Manifest>,
166 permission_files: BTreeMap<String, Vec<PermissionFile>>,
167}
168
169fn inline_plugins(
170 out_dir: &Path,
171 inlined_plugins: HashMap<&'static str, InlinedPlugin>,
172) -> Result<InlinedPluginsAcl> {
173 let mut acl_manifests = BTreeMap::new();
174 let mut permission_files_map = BTreeMap::new();
175
176 for (name, plugin) in inlined_plugins {
177 let plugin_out_dir = out_dir.join("plugins").join(name);
178 fs::create_dir_all(&plugin_out_dir)?;
179
180 let mut permission_files = if plugin.commands.is_empty() {
181 Vec::new()
182 } else {
183 let autogenerated = tauri_utils::acl::build::autogenerate_command_permissions(
184 &plugin_out_dir,
185 plugin.commands,
186 "",
187 false,
188 );
189
190 let default_permissions = plugin.default.map(|default| match default {
191 DefaultPermissionRule::AllowAllCommands => autogenerated.allowed,
192 DefaultPermissionRule::Allow(permissions) => permissions,
193 });
194 if let Some(default_permissions) = default_permissions {
195 let default_permissions = default_permissions
196 .iter()
197 .map(|p| format!("\"{p}\""))
198 .collect::<Vec<String>>()
199 .join(",");
200 let default_permission = format!(
201 r###"# Automatically generated - DO NOT EDIT!
202[default]
203permissions = [{default_permissions}]
204"###
205 );
206
207 let default_permission_path = plugin_out_dir.join("default.toml");
208
209 write_if_changed(&default_permission_path, default_permission)
210 .unwrap_or_else(|_| panic!("unable to autogenerate {default_permission_path:?}"));
211 }
212
213 tauri_utils::acl::build::define_permissions(
214 &PathBuf::from(glob::Pattern::escape(&plugin_out_dir.to_string_lossy()))
215 .join("*")
216 .to_string_lossy(),
217 name,
218 &plugin_out_dir,
219 |_| true,
220 )?
221 };
222
223 if let Some(pattern) = plugin.permissions_path_pattern {
224 permission_files.extend(tauri_utils::acl::build::define_permissions(
225 pattern,
226 name,
227 &plugin_out_dir,
228 |_| true,
229 )?);
230 } else {
231 let default_permissions_path = Path::new("permissions").join(name);
232 if default_permissions_path.exists() {
233 println!(
234 "cargo:rerun-if-changed={}",
235 default_permissions_path.display()
236 );
237 }
238 permission_files.extend(tauri_utils::acl::build::define_permissions(
239 &PathBuf::from(glob::Pattern::escape(
240 &default_permissions_path.to_string_lossy(),
241 ))
242 .join("**")
243 .join("*")
244 .to_string_lossy(),
245 name,
246 &plugin_out_dir,
247 |_| true,
248 )?);
249 }
250
251 permission_files_map.insert(name.into(), permission_files.clone());
252
253 let manifest = tauri_utils::acl::manifest::Manifest::new(permission_files, None);
254 acl_manifests.insert(name.into(), manifest);
255 }
256
257 Ok(InlinedPluginsAcl {
258 manifests: acl_manifests,
259 permission_files: permission_files_map,
260 })
261}
262
263#[derive(Debug)]
264struct AppManifestAcl {
265 manifest: Manifest,
266 permission_files: Vec<PermissionFile>,
267}
268
269fn app_manifest_permissions(
270 out_dir: &Path,
271 manifest: AppManifest,
272 inlined_plugins: &HashMap<&'static str, InlinedPlugin>,
273) -> Result<AppManifestAcl> {
274 let app_out_dir = out_dir.join("app-manifest");
275 fs::create_dir_all(&app_out_dir)?;
276 let pkg_name = "__app__";
277
278 let mut permission_files = if manifest.commands.is_empty() {
279 Vec::new()
280 } else {
281 let autogenerated_path = Path::new("./permissions/autogenerated");
282 tauri_utils::acl::build::autogenerate_command_permissions(
283 autogenerated_path,
284 manifest.commands,
285 "",
286 false,
287 );
288 tauri_utils::acl::build::define_permissions(
289 &autogenerated_path.join("*").to_string_lossy(),
290 pkg_name,
291 &app_out_dir,
292 |_| true,
293 )?
294 };
295
296 if let Some(pattern) = manifest.permissions_path_pattern {
297 permission_files.extend(tauri_utils::acl::build::define_permissions(
298 pattern,
299 pkg_name,
300 &app_out_dir,
301 |_| true,
302 )?);
303 } else {
304 let default_permissions_path = Path::new("permissions");
305 if default_permissions_path.exists() {
306 println!(
307 "cargo:rerun-if-changed={}",
308 default_permissions_path.display()
309 );
310 }
311
312 let permissions_root = env::current_dir()?.join("permissions");
313 let inlined_plugins_permissions: Vec<_> = inlined_plugins
314 .keys()
315 .map(|name| permissions_root.join(name))
316 .flat_map(|p| p.canonicalize())
317 .collect();
318
319 permission_files.extend(tauri_utils::acl::build::define_permissions(
320 &default_permissions_path
321 .join("**")
322 .join("*")
323 .to_string_lossy(),
324 pkg_name,
325 &app_out_dir,
326 |p| {
328 !inlined_plugins_permissions
329 .iter()
330 .any(|inlined_path| p.starts_with(inlined_path))
331 },
332 )?);
333 }
334
335 Ok(AppManifestAcl {
336 permission_files: permission_files.clone(),
337 manifest: tauri_utils::acl::manifest::Manifest::new(permission_files, None),
338 })
339}
340
341fn validate_capabilities(
342 acl_manifests: &BTreeMap<String, Manifest>,
343 capabilities: &BTreeMap<String, Capability>,
344) -> Result<()> {
345 let target = tauri_utils::platform::Target::from_triple(&std::env::var("TARGET").unwrap());
346
347 for capability in capabilities.values() {
348 if !capability
349 .platforms
350 .as_ref()
351 .map(|platforms| platforms.contains(&target))
352 .unwrap_or(true)
353 {
354 continue;
355 }
356
357 for permission_entry in &capability.permissions {
358 let permission_id = permission_entry.identifier();
359
360 let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
361 let permission_name = permission_id.get_base();
362
363 let permission_exists = acl_manifests
364 .get(key)
365 .map(|manifest| {
366 permission_name == "default"
368 || manifest.permissions.contains_key(permission_name)
369 || manifest.permission_sets.contains_key(permission_name)
370 })
371 .unwrap_or(false);
372
373 if !permission_exists {
374 let mut available_permissions = Vec::new();
375 for (key, manifest) in acl_manifests {
376 let prefix = if key == APP_ACL_KEY {
377 "".to_string()
378 } else {
379 format!("{key}:")
380 };
381 if manifest.default_permission.is_some() {
382 available_permissions.push(format!("{prefix}default"));
383 }
384 for p in manifest.permissions.keys() {
385 available_permissions.push(format!("{prefix}{p}"));
386 }
387 for p in manifest.permission_sets.keys() {
388 available_permissions.push(format!("{prefix}{p}"));
389 }
390 }
391
392 anyhow::bail!(
393 "Permission {} not found, expected one of {}",
394 permission_id.get(),
395 available_permissions.join(", ")
396 );
397 }
398 }
399 }
400
401 Ok(())
402}
403
404pub fn build(
405 out_dir: &Path,
406 target: Target,
407 config: &Config,
408 attributes: &Attributes,
409) -> super::Result<()> {
410 let mut acl_manifests = read_plugins_manifests()?;
411
412 let app_acl = app_manifest_permissions(
413 out_dir,
414 attributes.app_manifest,
415 &attributes.inlined_plugins,
416 )?;
417 let has_app_manifest = app_acl.manifest.default_permission.is_some()
418 || !app_acl.manifest.permission_sets.is_empty()
419 || !app_acl.manifest.permissions.is_empty();
420 if has_app_manifest {
421 acl_manifests.insert(APP_ACL_KEY.into(), app_acl.manifest);
422 }
423
424 let inline_plugins_acl = inline_plugins(out_dir, attributes.inlined_plugins.clone())?;
425
426 acl_manifests.extend(inline_plugins_acl.manifests);
427
428 let acl_manifests_path = save_acl_manifests(&acl_manifests)?;
429 fs::copy(acl_manifests_path, out_dir.join(ACL_MANIFESTS_FILE_NAME))?;
430
431 tauri_utils::acl::schema::generate_capability_schema(&acl_manifests, target)?;
432
433 let capabilities_from_files = if let Some(pattern) = attributes.capabilities_path_pattern {
434 tauri_utils::acl::build::parse_capabilities(pattern)?
435 } else {
436 let capabilities_dir = std::env::current_dir()
446 .context("resolve current dir for capabilities watch path")?
447 .join("capabilities");
448 println!("cargo:rerun-if-changed={}", capabilities_dir.display());
449 tauri_utils::acl::build::parse_capabilities("./capabilities/**/*")?
450 };
451 let capabilities = get_capabilities(config, capabilities_from_files.clone(), None)
458 .context("failed to resolve the capabilities from the Tauri configuration file")?;
459
460 validate_capabilities(&acl_manifests, &capabilities)?;
461
462 let capabilities_path = save_capabilities(&capabilities_from_files)?;
463 fs::copy(capabilities_path, out_dir.join(CAPABILITIES_FILE_NAME))?;
464
465 let mut permissions_map = inline_plugins_acl.permission_files;
466 if has_app_manifest {
467 permissions_map.insert(APP_ACL_KEY.to_string(), app_acl.permission_files);
468 }
469
470 tauri_utils::acl::build::generate_allowed_commands(
473 out_dir,
474 Some(capabilities_from_files),
475 permissions_map,
476 )?;
477
478 Ok(())
479}