origin_manifest/
profile.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "kebab-case")]
10pub enum SecurityProfile {
11 ReadonlyDashboard,
13
14 StandardDashboard,
16
17 AccountSettings,
20
21 LocalWorkspace,
28}
29
30impl SecurityProfile {
31 pub fn permissions(self) -> &'static [&'static str] {
37 match self {
38 Self::ReadonlyDashboard => &[
39 "core:default",
40 "core:event:allow-listen",
41 "core:event:allow-unlisten",
42 ],
43 Self::StandardDashboard => &[
44 "core:default",
45 "core:event:allow-listen",
46 "core:event:allow-unlisten",
47 ],
48 Self::AccountSettings => &[
49 "core:default",
50 "core:event:allow-listen",
51 "core:event:allow-unlisten",
52 "core:window:allow-close",
53 ],
54 Self::LocalWorkspace => &[
55 "core:default",
56 "core:event:allow-listen",
57 "core:event:allow-unlisten",
58 ],
59 }
60 }
61
62 pub fn identifier(self) -> &'static str {
63 match self {
64 Self::ReadonlyDashboard => "readonly-dashboard",
65 Self::StandardDashboard => "standard-dashboard",
66 Self::AccountSettings => "account-settings",
67 Self::LocalWorkspace => "local-workspace",
68 }
69 }
70
71 pub fn description(self) -> &'static str {
72 match self {
73 Self::ReadonlyDashboard => {
74 "Reads application state and receives platform events. No filesystem, \
75 no shell, no process execution."
76 }
77 Self::StandardDashboard => {
78 "Main window: reads application state and receives platform events. No \
79 filesystem, no shell, no process execution."
80 }
81 Self::AccountSettings => {
82 "Settings window: manages accounts through commands. Credentials never \
83 reach the frontend."
84 }
85 Self::LocalWorkspace => {
86 "Workspace window: access to workspace files and allowlisted processes \
87 via Origin commands. No direct Tauri fs or shell plugin access."
88 }
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn no_profile_grants_fs_shell_or_process() {
99 let profiles = [
100 SecurityProfile::ReadonlyDashboard,
101 SecurityProfile::StandardDashboard,
102 SecurityProfile::AccountSettings,
103 SecurityProfile::LocalWorkspace,
104 ];
105
106 for profile in profiles {
107 for permission in profile.permissions() {
108 assert!(
109 !permission.starts_with("fs:")
110 && !permission.starts_with("shell:")
111 && !permission.starts_with("process:"),
112 "{} grants {permission}",
113 profile.identifier()
114 );
115 }
116 }
117 }
118
119 #[test]
120 fn all_profiles_have_unique_identifiers() {
121 let profiles = [
122 SecurityProfile::ReadonlyDashboard,
123 SecurityProfile::StandardDashboard,
124 SecurityProfile::AccountSettings,
125 SecurityProfile::LocalWorkspace,
126 ];
127
128 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
129 for profile in profiles {
130 let id = profile.identifier();
131 assert!(seen.insert(id), "duplicate identifier: {id}");
132 assert!(
133 !profile.description().is_empty(),
134 "{id}: description must not be empty"
135 );
136 }
137 }
138
139 #[test]
140 fn local_workspace_round_trips_through_the_manifest_format() {
141 let parsed: SecurityProfile = toml::from_str("value = \"local-workspace\"")
142 .map(|table: toml::Table| table["value"].clone())
143 .map(|value| value.try_into().unwrap())
144 .unwrap();
145
146 assert_eq!(parsed, SecurityProfile::LocalWorkspace);
147 assert_eq!(parsed.identifier(), "local-workspace");
148 }
149
150 #[test]
151 fn profiles_round_trip_through_the_manifest_format() {
152 let parsed: SecurityProfile = toml::from_str("value = \"account-settings\"")
153 .map(|table: toml::Table| table["value"].clone())
154 .map(|value| value.try_into().unwrap())
155 .unwrap();
156
157 assert_eq!(parsed, SecurityProfile::AccountSettings);
158 assert_eq!(parsed.identifier(), "account-settings");
159 }
160}