kustos_shared/
subject.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use std::{fmt::Display, str::FromStr};
6
7use snafu::Snafu;
8use uuid::Uuid;
9
10/// The error type is returned when parsing invalid values from strings.
11///
12/// Derived using [snafu::Snafu]
13#[derive(Debug, Snafu)]
14pub enum ParsingError {
15    #[snafu(display("Invalid access method: `{method}`"))]
16    InvalidAccessMethod { method: String },
17
18    #[snafu(display("String was not a PolicyUser casbin string: `{user}`"))]
19    PolicyUser { user: String },
20
21    #[snafu(display("String was not a PolicyInvite casbin string: `{invite}`"))]
22    PolicyInvite { invite: String },
23
24    #[snafu(display("String was not a PolicyInternalGroup casbin string: `{group}"))]
25    PolicyInternalGroup { group: String },
26
27    #[snafu(display("String was not a PolicyOPGroup casbin string: `{group}`"))]
28    PolicyOPGroup { group: String },
29
30    #[snafu(display("Invalid UUID: {source}"), context(false))]
31    Uuid {
32        #[snafu(source(from(uuid::Error, Box::new)))]
33        source: Box<uuid::Error>,
34    },
35
36    #[snafu(display("Custom: {message}"), whatever)]
37    Custom {
38        message: String,
39
40        #[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, Some)))]
41        source: Option<Box<dyn std::error::Error + Send + Sync>>,
42    },
43}
44
45/// Trait to tag a type as a subject
46///
47/// Types tagged with this trait need to implement the underlying internal conversion types as well.
48/// The internal implementations take care of that for the subjects that are part of this API.
49pub trait IsSubject {}
50
51/// A uuid backed user identifier.
52///
53/// This crates requires your users to be identifiable by a uuid.
54#[derive(Debug, PartialEq, Eq, Clone, Hash)]
55pub struct PolicyUser(pub(crate) uuid::Uuid);
56
57impl IsSubject for PolicyUser {}
58
59impl PolicyUser {
60    /// Create a ZERO policy user, e.g. for testing purposes
61    pub const fn nil() -> Self {
62        Self(Uuid::nil())
63    }
64
65    /// Create a policy user from a number, e.g. for testing purposes
66    pub const fn from_u128(id: u128) -> Self {
67        Self(Uuid::from_u128(id))
68    }
69
70    /// Generate a new random policy user
71    pub fn generate() -> Self {
72        Self(Uuid::new_v4())
73    }
74}
75
76impl Display for PolicyUser {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.0)
79    }
80}
81
82impl From<uuid::Uuid> for PolicyUser {
83    fn from(user: uuid::Uuid) -> Self {
84        PolicyUser(user)
85    }
86}
87
88impl FromStr for PolicyUser {
89    type Err = ParsingError;
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        if s.starts_with("user::") {
93            Ok(PolicyUser(uuid::Uuid::from_str(
94                s.trim_start_matches("user::"),
95            )?))
96        } else {
97            PolicyUserSnafu { user: s.to_owned() }.fail()
98        }
99    }
100}
101
102impl AsRef<uuid::Uuid> for PolicyUser {
103    fn as_ref(&self) -> &uuid::Uuid {
104        &self.0
105    }
106}
107
108/// A uuid backed invite identifier.
109///
110/// This crates requires the invites codes to be identifiable by a uuid.
111#[derive(Debug, PartialEq, Eq, Clone, Hash)]
112pub struct PolicyInvite(pub(crate) uuid::Uuid);
113
114impl IsSubject for PolicyInvite {}
115
116impl PolicyInvite {
117    /// Create a ZERO policy invite, e.g. for testing purposes
118    pub const fn nil() -> Self {
119        Self(Uuid::nil())
120    }
121
122    /// Create a policy invite from a number, e.g. for testing purposes
123    pub const fn from_u128(id: u128) -> Self {
124        Self(Uuid::from_u128(id))
125    }
126
127    /// Generate a new random policy invite
128    pub fn generate() -> Self {
129        Self(Uuid::new_v4())
130    }
131}
132
133impl From<uuid::Uuid> for PolicyInvite {
134    fn from(invite: uuid::Uuid) -> Self {
135        PolicyInvite(invite)
136    }
137}
138
139impl FromStr for PolicyInvite {
140    type Err = ParsingError;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        if s.starts_with("invite::") {
144            Ok(PolicyInvite(uuid::Uuid::from_str(
145                s.trim_start_matches("invite::"),
146            )?))
147        } else {
148            PolicyInviteSnafu {
149                invite: s.to_owned(),
150            }
151            .fail()
152        }
153    }
154}
155
156impl AsRef<uuid::Uuid> for PolicyInvite {
157    fn as_ref(&self) -> &uuid::Uuid {
158        &self.0
159    }
160}
161
162/// An internal group e.g. administrator, moderator, etc.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct PolicyRole(pub(crate) String);
165
166impl IsSubject for PolicyRole {}
167
168impl From<String> for PolicyRole {
169    fn from(group: String) -> Self {
170        PolicyRole(group)
171    }
172}
173
174impl From<&str> for PolicyRole {
175    fn from(group: &str) -> Self {
176        PolicyRole(group.to_string())
177    }
178}
179
180impl FromStr for PolicyRole {
181    type Err = ParsingError;
182
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        if s.starts_with("role::") {
185            Ok(PolicyRole(s.trim_start_matches("role::").to_string()))
186        } else {
187            PolicyInternalGroupSnafu {
188                group: s.to_owned(),
189            }
190            .fail()
191        }
192    }
193}
194
195impl AsRef<str> for PolicyRole {
196    fn as_ref(&self) -> &str {
197        self.0.as_ref()
198    }
199}
200
201/// A user defined group, such as information from keycloak or LDAP
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct PolicyGroup(pub(crate) String);
204
205impl IsSubject for PolicyGroup {}
206
207impl From<String> for PolicyGroup {
208    fn from(group: String) -> Self {
209        PolicyGroup(group)
210    }
211}
212impl From<&str> for PolicyGroup {
213    fn from(group: &str) -> Self {
214        PolicyGroup(group.to_string())
215    }
216}
217
218impl FromStr for PolicyGroup {
219    type Err = ParsingError;
220
221    fn from_str(s: &str) -> Result<Self, Self::Err> {
222        if s.starts_with("group::") {
223            Ok(PolicyGroup(s.trim_start_matches("group::").to_string()))
224        } else {
225            PolicyOPGroupSnafu {
226                group: s.to_owned(),
227            }
228            .fail()
229        }
230    }
231}
232
233/// Maps a PolicyUser to a PolicyRole
234pub struct UserToRole(pub PolicyUser, pub PolicyRole);
235
236/// Maps a PolicyUser to a PolicyGroup
237pub struct UserToGroup(pub PolicyUser, pub PolicyGroup);
238
239/// Maps a PolicyGroup to a PolicyRole
240pub struct GroupToRole(pub PolicyGroup, pub PolicyRole);
241
242#[cfg(test)]
243mod tests {
244    use std::str::FromStr;
245
246    use crate::subject::{ParsingError, PolicyInvite, PolicyUser};
247
248    #[test]
249    fn test_policy_invite_invalid_uuid() {
250        let raw_invite = "invite::00000000-0000-0000c0000-000000000000";
251        let parsing_result = PolicyInvite::from_str(raw_invite);
252        assert!(
253            matches!(parsing_result, Err(ParsingError::Uuid { source: _ }),),
254            "Expected Uuid error, Got: {parsing_result:?}",
255        );
256    }
257
258    #[test]
259    fn test_policy_user_invalid_uuid() {
260        let raw_invite = "user::00000000-0000-0000c0000-000000000000";
261        let parsing_result = PolicyUser::from_str(raw_invite);
262        assert!(
263            matches!(parsing_result, Err(ParsingError::Uuid { source: _ }),),
264            "Expected Uuid error, Got: {parsing_result:?}",
265        );
266    }
267}