use serde::{Deserialize, Serialize};
use std::{num::NonZeroU64, path::PathBuf, str::FromStr, sync::Arc};
use thiserror::Error;
use url::Url;
use crate::platform::Target;
pub use self::{identifier::*, value::*};
pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
pub const APP_ACL_KEY: &str = "__app-acl__";
pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
#[cfg(feature = "build")]
pub mod build;
pub mod capability;
pub mod identifier;
pub mod manifest;
pub mod resolved;
#[cfg(feature = "schema")]
pub mod schema;
pub mod value;
#[derive(Debug, Error)]
pub enum Error {
#[error("expected build script env var {0}, but it was not found - ensure this is called in a build script")]
BuildVar(&'static str),
#[error("package.links field in the Cargo manifest is not set, it should be set to the same as package.name")]
LinksMissing,
#[error(
"package.links field in the Cargo manifest MUST be set to the same value as package.name"
)]
LinksName,
#[error("failed to read file '{}': {}", _1.display(), _0)]
ReadFile(std::io::Error, PathBuf),
#[error("failed to write file '{}': {}", _1.display(), _0)]
WriteFile(std::io::Error, PathBuf),
#[error("failed to create file '{}': {}", _1.display(), _0)]
CreateFile(std::io::Error, PathBuf),
#[error("failed to create dir '{}': {}", _1.display(), _0)]
CreateDir(std::io::Error, PathBuf),
#[cfg(feature = "build")]
#[error("failed to execute: {0}")]
Metadata(#[from] ::cargo_metadata::Error),
#[error("failed to run glob: {0}")]
Glob(#[from] glob::PatternError),
#[error("failed to parse TOML: {0}")]
Toml(#[from] toml::de::Error),
#[error("failed to parse JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("unknown permission format {0}")]
UnknownPermissionFormat(String),
#[error("unknown capability format {0}")]
UnknownCapabilityFormat(String),
#[error("permission {permission} not found from set {set}")]
SetPermissionNotFound {
permission: String,
set: String,
},
#[error("unknown ACL for {key}, expected one of {available}")]
UnknownManifest {
key: String,
available: String,
},
#[error("unknown permission {permission} for {key}")]
UnknownPermission {
key: String,
permission: String,
},
#[error("capability with identifier `{identifier}` already exists")]
CapabilityAlreadyExists {
identifier: String,
},
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Commands {
#[serde(default)]
pub allow: Vec<String>,
#[serde(default)]
pub deny: Vec<String>,
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Scopes {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deny: Option<Vec<Value>>,
}
impl Scopes {
fn is_empty(&self) -> bool {
self.allow.is_none() && self.deny.is_none()
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Permission {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<NonZeroU64>,
pub identifier: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub commands: Commands,
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
pub scope: Scopes,
#[serde(skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<Target>>,
}
impl Permission {
pub fn is_active(&self, target: &Target) -> bool {
self
.platforms
.as_ref()
.map(|platforms| platforms.contains(target))
.unwrap_or(true)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionSet {
pub identifier: String,
pub description: String,
pub permissions: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
impl FromStr for RemoteUrlPattern {
type Err = urlpattern::quirks::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(s, None)?;
if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.search.replace("*".to_string());
}
if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) {
init.hash.replace("*".to_string());
}
if init
.pathname
.as_ref()
.map(|p| p.is_empty() || p == "/")
.unwrap_or(true)
{
init.pathname.replace("*".to_string());
}
let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
Ok(Self(Arc::new(pattern), s.to_string()))
}
}
impl RemoteUrlPattern {
#[doc(hidden)]
pub fn as_str(&self) -> &str {
&self.1
}
pub fn test(&self, url: &Url) -> bool {
self
.0
.test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
.unwrap_or_default()
}
}
impl PartialEq for RemoteUrlPattern {
fn eq(&self, other: &Self) -> bool {
self.0.protocol() == other.0.protocol()
&& self.0.username() == other.0.username()
&& self.0.password() == other.0.password()
&& self.0.hostname() == other.0.hostname()
&& self.0.port() == other.0.port()
&& self.0.pathname() == other.0.pathname()
&& self.0.search() == other.0.search()
&& self.0.hash() == other.0.hash()
}
}
impl Eq for RemoteUrlPattern {}
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum ExecutionContext {
#[default]
Local,
Remote {
url: RemoteUrlPattern,
},
}
#[cfg(test)]
mod tests {
use crate::acl::RemoteUrlPattern;
#[test]
fn url_pattern_domain_wildcard() {
let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_path_wildcard() {
let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
}
#[test]
fn url_pattern_scheme_wildcard() {
let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
}
}
#[cfg(feature = "build")]
mod build_ {
use std::convert::identity;
use crate::{literal_struct, tokens::*};
use super::*;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
impl ToTokens for ExecutionContext {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
tokens.append_all(match self {
Self::Local => {
quote! { #prefix::Local }
}
Self::Remote { url } => {
let url = url.as_str();
quote! { #prefix::Remote { url: #url.parse().unwrap() } }
}
});
}
}
impl ToTokens for Commands {
fn to_tokens(&self, tokens: &mut TokenStream) {
let allow = vec_lit(&self.allow, str_lit);
let deny = vec_lit(&self.deny, str_lit);
literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
}
}
impl ToTokens for Scopes {
fn to_tokens(&self, tokens: &mut TokenStream) {
let allow = opt_vec_lit(self.allow.as_ref(), identity);
let deny = opt_vec_lit(self.deny.as_ref(), identity);
literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
}
}
impl ToTokens for Permission {
fn to_tokens(&self, tokens: &mut TokenStream) {
let version = opt_lit_owned(self.version.as_ref().map(|v| {
let v = v.get();
quote!(::core::num::NonZeroU64::new(#v).unwrap())
}));
let identifier = str_lit(&self.identifier);
let description = opt_str_lit(self.description.as_ref());
let commands = &self.commands;
let scope = &self.scope;
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
literal_struct!(
tokens,
::tauri::utils::acl::Permission,
version,
identifier,
description,
commands,
scope,
platforms
)
}
}
impl ToTokens for PermissionSet {
fn to_tokens(&self, tokens: &mut TokenStream) {
let identifier = str_lit(&self.identifier);
let description = str_lit(&self.description);
let permissions = vec_lit(&self.permissions, str_lit);
literal_struct!(
tokens,
::tauri::utils::acl::PermissionSet,
identifier,
description,
permissions
)
}
}
}