Skip to main content

rustauth_plugins/organization/
permissions.rs

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}