1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use crate::access::Role;
5
6use super::options::OrganizationOptions;
7use super::OrganizationRoleRecord;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum OrganizationRole {
11 Owner,
12 Admin,
13 Member,
14}
15
16impl OrganizationRole {
17 pub fn as_str(self) -> &'static str {
18 match self {
19 Self::Owner => "owner",
20 Self::Admin => "admin",
21 Self::Member => "member",
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum OrganizationPermission {
28 OrganizationUpdate,
29 OrganizationDelete,
30 MemberCreate,
31 MemberUpdate,
32 MemberDelete,
33 InvitationCreate,
34 InvitationCancel,
35 TeamCreate,
36 TeamUpdate,
37 TeamDelete,
38 AcCreate,
39 AcRead,
40 AcUpdate,
41 AcDelete,
42 ApiKeyCreate,
43 ApiKeyRead,
44 ApiKeyUpdate,
45 ApiKeyDelete,
46}
47
48impl OrganizationPermission {
49 pub(crate) fn resource_action(self) -> (&'static str, &'static str) {
50 match self {
51 Self::OrganizationUpdate => ("organization", "update"),
52 Self::OrganizationDelete => ("organization", "delete"),
53 Self::MemberCreate => ("member", "create"),
54 Self::MemberUpdate => ("member", "update"),
55 Self::MemberDelete => ("member", "delete"),
56 Self::InvitationCreate => ("invitation", "create"),
57 Self::InvitationCancel => ("invitation", "cancel"),
58 Self::TeamCreate => ("team", "create"),
59 Self::TeamUpdate => ("team", "update"),
60 Self::TeamDelete => ("team", "delete"),
61 Self::AcCreate => ("ac", "create"),
62 Self::AcRead => ("ac", "read"),
63 Self::AcUpdate => ("ac", "update"),
64 Self::AcDelete => ("ac", "delete"),
65 Self::ApiKeyCreate => ("apiKey", "create"),
66 Self::ApiKeyRead => ("apiKey", "read"),
67 Self::ApiKeyUpdate => ("apiKey", "update"),
68 Self::ApiKeyDelete => ("apiKey", "delete"),
69 }
70 }
71}
72
73pub fn has_permission(
74 role: &str,
75 options: &OrganizationOptions,
76 permission: OrganizationPermission,
77) -> bool {
78 role.split(',').map(str::trim).any(|role| {
79 if role == options.creator_role {
80 return true;
81 }
82 if configured_role_has_permission(role, options, permission) {
83 return true;
84 }
85 if custom_role_has_permission(role, options, permission) {
86 return true;
87 }
88 match role {
89 "owner" => true,
90 "admin" => !matches!(permission, OrganizationPermission::OrganizationDelete),
91 "member" => matches!(permission, OrganizationPermission::AcRead),
92 _ => false,
93 }
94 })
95}
96
97pub(crate) fn role_has_resource_action(
98 role: &str,
99 options: &OrganizationOptions,
100 resource: &str,
101 action: &str,
102) -> bool {
103 role.split(',').map(str::trim).any(|role| {
104 if role == options.creator_role {
105 return true;
106 }
107 if options
108 .roles
109 .as_ref()
110 .and_then(|roles| roles.get(role))
111 .is_some_and(|role| role_has_resource_action_statement(role, resource, action))
112 {
113 return true;
114 }
115 if options.custom_roles.get(role).is_some_and(|permission| {
116 permission_value_has_resource_action(permission, resource, action)
117 }) {
118 return true;
119 }
120 resolve_permission(resource, action)
121 .map(|permission| match role {
122 "owner" => true,
123 "admin" => !matches!(permission, OrganizationPermission::OrganizationDelete),
124 "member" => matches!(permission, OrganizationPermission::AcRead),
125 _ => false,
126 })
127 .unwrap_or(false)
128 })
129}
130
131pub(crate) fn role_has_resource_action_with_dynamic(
132 role: &str,
133 options: &OrganizationOptions,
134 dynamic_roles: &[OrganizationRoleRecord],
135 resource: &str,
136 action: &str,
137) -> bool {
138 role_has_resource_action(role, options, resource, action)
139 || role.split(',').map(str::trim).any(|role| {
140 dynamic_roles.iter().any(|record| {
141 record.role == role
142 && permission_value_has_resource_action(&record.permission, resource, action)
143 })
144 })
145}
146
147pub(crate) fn missing_permissions(
148 role: &str,
149 options: &OrganizationOptions,
150 dynamic_roles: &[OrganizationRoleRecord],
151 permission: &serde_json::Value,
152) -> BTreeMap<String, Vec<String>> {
153 let Some(object) = permission.as_object() else {
154 return BTreeMap::new();
155 };
156 let mut missing = BTreeMap::new();
157 for (resource, actions) in object {
158 let Some(actions) = actions.as_array() else {
159 continue;
160 };
161 let missing_actions = actions
162 .iter()
163 .filter_map(serde_json::Value::as_str)
164 .filter(|action| {
165 !role_has_resource_action_with_dynamic(
166 role,
167 options,
168 dynamic_roles,
169 resource,
170 action,
171 )
172 })
173 .map(str::to_owned)
174 .collect::<Vec<_>>();
175 if !missing_actions.is_empty() {
176 missing.insert(resource.clone(), missing_actions);
177 }
178 }
179 missing
180}
181
182pub(crate) fn parse_roles(role: impl AsRef<str>) -> String {
183 role.as_ref()
184 .split(',')
185 .map(str::trim)
186 .filter(|role| !role.is_empty())
187 .collect::<Vec<_>>()
188 .join(",")
189}
190
191pub(crate) fn is_known_static_role(role: &str, options: &OrganizationOptions) -> bool {
192 role == options.creator_role
193 || options.custom_roles.contains_key(role)
194 || options
195 .roles
196 .as_ref()
197 .is_some_and(|roles| roles.contains_key(role))
198 || matches!(role, "owner" | "admin" | "member")
199}
200
201pub(crate) fn permission_value_has_permission(
202 permission: &serde_json::Value,
203 required: OrganizationPermission,
204) -> bool {
205 let (resource, action) = required.resource_action();
206 permission_value_has_resource_action(permission, resource, action)
207}
208
209pub(crate) fn permission_value_has_resource_action(
210 permission: &serde_json::Value,
211 resource: &str,
212 action: &str,
213) -> bool {
214 permission
215 .get(resource)
216 .or_else(|| {
217 (resource == "apiKey")
218 .then(|| permission.get("api_key"))
219 .flatten()
220 })
221 .and_then(serde_json::Value::as_array)
222 .map(|actions| actions.iter().any(|value| value.as_str() == Some(action)))
223 .unwrap_or(false)
224}
225
226pub(crate) fn validate_permission_with_access_control(
227 permission: &serde_json::Value,
228 options: &OrganizationOptions,
229) -> Result<(), rustauth_core::error::RustAuthError> {
230 let Some(ac) = options.access_control.as_ref() else {
231 return Err(rustauth_core::error::RustAuthError::Api(
232 "MISSING_AC_INSTANCE".to_owned(),
233 ));
234 };
235 let statements = permission_value_to_statements(permission)?;
236 ac.new_role(statements)
237 .map(|_| ())
238 .map_err(|error| rustauth_core::error::RustAuthError::InvalidConfig(error.to_string()))
239}
240
241fn configured_role_has_permission(
242 role: &str,
243 options: &OrganizationOptions,
244 permission: OrganizationPermission,
245) -> bool {
246 options
247 .roles
248 .as_ref()
249 .and_then(|roles| roles.get(role))
250 .map(|role| role_has_permission(role, permission))
251 .unwrap_or(false)
252}
253
254fn role_has_resource_action_statement(role: &Role, resource: &str, action: &str) -> bool {
255 role.statements()
256 .get(resource)
257 .or_else(|| {
258 (resource == "apiKey")
259 .then(|| role.statements().get("api_key"))
260 .flatten()
261 })
262 .is_some_and(|actions| actions.contains(action))
263}
264
265fn custom_role_has_permission(
266 role: &str,
267 options: &OrganizationOptions,
268 permission: OrganizationPermission,
269) -> bool {
270 options
271 .custom_roles
272 .get(role)
273 .map(|value| permission_value_has_permission(value, permission))
274 .unwrap_or(false)
275}
276
277fn role_has_permission(role: &Role, permission: OrganizationPermission) -> bool {
278 let (resource, action) = permission.resource_action();
279 role_has_resource_action_statement(role, resource, action)
280}
281
282fn permission_value_to_statements(
283 permission: &serde_json::Value,
284) -> Result<crate::access::Statements, rustauth_core::error::RustAuthError> {
285 let Some(object) = permission.as_object() else {
286 return Err(rustauth_core::error::RustAuthError::Api(
287 "permission must be an object".to_owned(),
288 ));
289 };
290 let mut statements = crate::access::Statements::new();
291 for (resource, actions) in object {
292 let Some(actions) = actions.as_array() else {
293 return Err(rustauth_core::error::RustAuthError::Api(
294 "permission actions must be arrays".to_owned(),
295 ));
296 };
297 statements.insert(
298 resource.clone(),
299 actions
300 .iter()
301 .filter_map(serde_json::Value::as_str)
302 .map(str::to_owned)
303 .collect(),
304 );
305 }
306 Ok(statements)
307}
308
309pub(crate) fn resolve_permission(resource: &str, action: &str) -> Option<OrganizationPermission> {
310 match (resource, action) {
311 ("organization", "update") => Some(OrganizationPermission::OrganizationUpdate),
312 ("organization", "delete") => Some(OrganizationPermission::OrganizationDelete),
313 ("member", "create") => Some(OrganizationPermission::MemberCreate),
314 ("member", "update") => Some(OrganizationPermission::MemberUpdate),
315 ("member", "delete") => Some(OrganizationPermission::MemberDelete),
316 ("invitation", "create") => Some(OrganizationPermission::InvitationCreate),
317 ("invitation", "cancel") => Some(OrganizationPermission::InvitationCancel),
318 ("team", "create") => Some(OrganizationPermission::TeamCreate),
319 ("team", "update") => Some(OrganizationPermission::TeamUpdate),
320 ("team", "delete") => Some(OrganizationPermission::TeamDelete),
321 ("ac", "create") => Some(OrganizationPermission::AcCreate),
322 ("ac", "read") => Some(OrganizationPermission::AcRead),
323 ("ac", "update") => Some(OrganizationPermission::AcUpdate),
324 ("ac", "delete") => Some(OrganizationPermission::AcDelete),
325 ("apiKey" | "api_key", "create") => Some(OrganizationPermission::ApiKeyCreate),
326 ("apiKey" | "api_key", "read") => Some(OrganizationPermission::ApiKeyRead),
327 ("apiKey" | "api_key", "update") => Some(OrganizationPermission::ApiKeyUpdate),
328 ("apiKey" | "api_key", "delete") => Some(OrganizationPermission::ApiKeyDelete),
329 _ => None,
330 }
331}