tauri_plugin_biometry/scope.rs
1//! Command-scope allowlist for the storage commands (`has_data`, `get_data`,
2//! `set_data`, `remove_data`).
3//!
4//! Each capability that grants one of these permissions can constrain the
5//! `domain` / `name` pairs the granted webview is allowed to touch:
6//!
7//! ```json
8//! {
9//! "identifier": "biometry:allow-get-data",
10//! "allow": [
11//! { "domain": "com.myapp.creds" },
12//! { "domain": "com.myapp.tokens", "name": "session-token" }
13//! ],
14//! "deny": [
15//! { "domain": "com.myapp.creds", "name": "master-key" }
16//! ]
17//! }
18//! ```
19//!
20//! Semantics:
21//! - An entry with `name` omitted matches **any** name in that domain.
22//! - `deny` is evaluated first and beats `allow`.
23//! - An empty `allow` list rejects every call — apps must opt in to the
24//! domains they actually use. This is the intentional secure default.
25
26use serde::{Deserialize, Serialize};
27use tauri::ipc::CommandScope;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Entry {
31 /// Domain this entry matches.
32 pub domain: String,
33 /// Optional exact-match name. Omit to match every name in `domain`.
34 #[serde(default)]
35 pub name: Option<String>,
36}
37
38impl Entry {
39 fn matches(&self, domain: &str, name: &str) -> bool {
40 self.domain == domain && self.name.as_deref().map_or(true, |n| n == name)
41 }
42}
43
44/// Checks `(domain, name)` against the scope merged into the calling
45/// webview's capability set. Returns `Err` if denied or not explicitly
46/// allowed.
47pub fn check(scope: &CommandScope<Entry>, domain: &str, name: &str) -> crate::Result<()> {
48 if scope.denies().iter().any(|e| e.matches(domain, name)) {
49 return Err(reject(domain, name, "denied by capability scope"));
50 }
51 if scope.allows().iter().any(|e| e.matches(domain, name)) {
52 return Ok(());
53 }
54 Err(reject(
55 domain,
56 name,
57 "not in capability allow-list — declare the (domain, name) in the capability's `allow` array",
58 ))
59}
60
61fn reject(domain: &str, name: &str, why: &str) -> crate::Error {
62 // Cross-platform path: scope.rs is shared with mobile, where
63 // `crate::error::PluginInvokeError` doesn't exist. The unified
64 // `crate::Error::Io` variant works everywhere, and the `scopeDenied:`
65 // prefix preserves the distinguishable error code in the message.
66 crate::Error::Io(std::io::Error::other(format!(
67 "scopeDenied: biometry ({domain}, {name}) {why}"
68 )))
69}