tauri_build/
acl.rs

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