use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SecurityProfile {
ReadonlyDashboard,
StandardDashboard,
AccountSettings,
LocalWorkspace,
}
impl SecurityProfile {
pub fn permissions(self) -> &'static [&'static str] {
match self {
Self::ReadonlyDashboard => &[
"core:default",
"core:event:allow-listen",
"core:event:allow-unlisten",
],
Self::StandardDashboard => &[
"core:default",
"core:event:allow-listen",
"core:event:allow-unlisten",
],
Self::AccountSettings => &[
"core:default",
"core:event:allow-listen",
"core:event:allow-unlisten",
"core:window:allow-close",
],
Self::LocalWorkspace => &[
"core:default",
"core:event:allow-listen",
"core:event:allow-unlisten",
],
}
}
pub fn identifier(self) -> &'static str {
match self {
Self::ReadonlyDashboard => "readonly-dashboard",
Self::StandardDashboard => "standard-dashboard",
Self::AccountSettings => "account-settings",
Self::LocalWorkspace => "local-workspace",
}
}
pub fn description(self) -> &'static str {
match self {
Self::ReadonlyDashboard => {
"Reads application state and receives platform events. No filesystem, \
no shell, no process execution."
}
Self::StandardDashboard => {
"Main window: reads application state and receives platform events. No \
filesystem, no shell, no process execution."
}
Self::AccountSettings => {
"Settings window: manages accounts through commands. Credentials never \
reach the frontend."
}
Self::LocalWorkspace => {
"Workspace window: access to workspace files and allowlisted processes \
via Origin commands. No direct Tauri fs or shell plugin access."
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_profile_grants_fs_shell_or_process() {
let profiles = [
SecurityProfile::ReadonlyDashboard,
SecurityProfile::StandardDashboard,
SecurityProfile::AccountSettings,
SecurityProfile::LocalWorkspace,
];
for profile in profiles {
for permission in profile.permissions() {
assert!(
!permission.starts_with("fs:")
&& !permission.starts_with("shell:")
&& !permission.starts_with("process:"),
"{} grants {permission}",
profile.identifier()
);
}
}
}
#[test]
fn all_profiles_have_unique_identifiers() {
let profiles = [
SecurityProfile::ReadonlyDashboard,
SecurityProfile::StandardDashboard,
SecurityProfile::AccountSettings,
SecurityProfile::LocalWorkspace,
];
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for profile in profiles {
let id = profile.identifier();
assert!(seen.insert(id), "duplicate identifier: {id}");
assert!(
!profile.description().is_empty(),
"{id}: description must not be empty"
);
}
}
#[test]
fn local_workspace_round_trips_through_the_manifest_format() {
let parsed: SecurityProfile = toml::from_str("value = \"local-workspace\"")
.map(|table: toml::Table| table["value"].clone())
.map(|value| value.try_into().unwrap())
.unwrap();
assert_eq!(parsed, SecurityProfile::LocalWorkspace);
assert_eq!(parsed.identifier(), "local-workspace");
}
#[test]
fn profiles_round_trip_through_the_manifest_format() {
let parsed: SecurityProfile = toml::from_str("value = \"account-settings\"")
.map(|table: toml::Table| table["value"].clone())
.map(|value| value.try_into().unwrap())
.unwrap();
assert_eq!(parsed, SecurityProfile::AccountSettings);
assert_eq!(parsed.identifier(), "account-settings");
}
}