reinfer_client/resources/
user.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_with::{DeserializeFromStr, SerializeDisplay};
4use std::{
5    collections::{HashMap, HashSet},
6    fmt::Display,
7    str::FromStr,
8};
9
10use super::project::ProjectName;
11use crate::error::{Error, Result};
12
13#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
14pub struct Id(pub String);
15
16impl FromStr for Id {
17    type Err = Error;
18
19    fn from_str(string: &str) -> Result<Self> {
20        if string.chars().all(|c| c.is_ascii_hexdigit()) {
21            Ok(Id(string.into()))
22        } else {
23            Err(Error::BadUserIdentifier {
24                identifier: string.into(),
25            })
26        }
27    }
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
31pub struct Username(pub String);
32
33impl FromStr for Username {
34    type Err = Error;
35
36    fn from_str(string: &str) -> Result<Self> {
37        if string
38            .chars()
39            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
40        {
41            Ok(Username(string.into()))
42        } else {
43            Err(Error::BadUserIdentifier {
44                identifier: string.into(),
45            })
46        }
47    }
48}
49
50#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
51pub struct Email(pub String);
52
53impl FromStr for Email {
54    type Err = Error;
55
56    fn from_str(string: &str) -> Result<Self> {
57        Ok(Email(string.into()))
58    }
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
62pub enum Identifier {
63    Id(Id),
64}
65
66impl FromStr for Identifier {
67    type Err = Error;
68
69    fn from_str(string: &str) -> Result<Self> {
70        if string.chars().all(|c| c.is_ascii_hexdigit()) {
71            Ok(Identifier::Id(Id(string.into())))
72        } else {
73            Err(Error::BadUserIdentifier {
74                identifier: string.into(),
75            })
76        }
77    }
78}
79
80#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
81pub struct User {
82    pub id: Id,
83    pub username: Username,
84    pub email: Email,
85    #[serde(rename = "created")]
86    pub created_at: DateTime<Utc>,
87    pub global_permissions: HashSet<GlobalPermission>,
88    #[serde(rename = "organisation_permissions")]
89    pub project_permissions: HashMap<ProjectName, HashSet<ProjectPermission>>,
90    pub sso_global_permissions: HashSet<GlobalPermission>,
91    pub verified: bool,
92}
93
94#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
95pub struct NewUser<'r> {
96    pub username: &'r Username,
97    pub email: &'r Email,
98    pub global_permissions: &'r [GlobalPermission],
99    #[serde(rename = "organisation_permissions")]
100    pub project_permissions: &'r HashMap<ProjectName, HashSet<ProjectPermission>>,
101}
102
103#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
104pub struct ModifiedPermissions<'r> {
105    #[serde(
106        rename = "organisation_permissions",
107        skip_serializing_if = "HashMap::is_empty"
108    )]
109    pub project_permissions: &'r HashMap<ProjectName, HashSet<ProjectPermission>>,
110    #[serde(skip_serializing_if = "Vec::is_empty")]
111    pub global_permissions: Vec<&'r GlobalPermission>,
112}
113
114#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
115pub(crate) struct CreateRequest<'request> {
116    pub user: NewUser<'request>,
117}
118
119#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
120pub(crate) struct CreateResponse {
121    pub user: User,
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
125pub(crate) struct GetAvailableResponse {
126    pub users: Vec<User>,
127}
128
129#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
130pub(crate) struct GetCurrentResponse {
131    pub user: User,
132}
133
134#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
135pub(crate) struct GetResponse {
136    pub user: User,
137}
138
139#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
140pub(crate) struct WelcomeEmailResponse {}
141
142#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
143#[serde(untagged)]
144pub enum ProjectPermission {
145    // TODO(jcalero)[RE-978] There is a bug with the implementation of this enum that causes
146    // deserialization of non-Unknown properties to fail. See
147    // [RE-978](https://reinfer.atlassian.net/browse/RE-978) for more info.
148    #[serde(rename = "sources-add-comments")]
149    CommentsAdmin,
150
151    #[serde(rename = "datasets-admin")]
152    DatasetsAdmin,
153
154    #[serde(rename = "voc")]
155    DatasetsWrite,
156
157    #[serde(rename = "datasets-review")]
158    DatasetsReview,
159
160    #[serde(rename = "voc-readonly")]
161    DatasetsRead,
162
163    #[serde(rename = "datasets-export")]
164    DatasetsExport,
165
166    #[serde(rename = "sources-admin")]
167    SourcesAdmin,
168
169    #[serde(rename = "sources-translate")]
170    SourcesTranslate,
171
172    #[serde(rename = "sources-read")]
173    SourcesRead,
174
175    #[serde(rename = "sources-read-sensitive")]
176    SourcesReadSensitive,
177
178    #[serde(rename = "streams-admin")]
179    StreamsAdmin,
180
181    #[serde(rename = "streams-consume")]
182    StreamsConsume,
183
184    #[serde(rename = "streams-read")]
185    StreamsRead,
186
187    #[serde(rename = "streams-write")]
188    StreamsWrite,
189
190    #[serde(rename = "users-read")]
191    UsersRead,
192
193    #[serde(rename = "users-write")]
194    UsersWrite,
195
196    #[serde(rename = "buckets-read")]
197    BucketsRead,
198
199    #[serde(rename = "buckets-write")]
200    BucketsWrite,
201
202    #[serde(rename = "buckets-append")]
203    BucketsAppend,
204
205    #[serde(rename = "files-write")]
206    FilesWrite,
207
208    #[serde(rename = "appliance-config-read")]
209    ApplianceConfigRead,
210
211    #[serde(rename = "appliance-config-write")]
212    ApplianceConfigWrite,
213
214    #[serde(rename = "integrations-read")]
215    IntegrationsRead,
216
217    #[serde(rename = "integrations-write")]
218    IntegrationsWrite,
219
220    Unknown(Box<str>),
221}
222
223impl FromStr for ProjectPermission {
224    type Err = Error;
225
226    fn from_str(string: &str) -> Result<Self> {
227        serde_json::de::from_str(&format!("\"{string}\"")).map_err(|_| {
228            Error::BadProjectPermission {
229                permission: string.into(),
230            }
231        })
232    }
233}
234
235#[derive(Debug, Clone, DeserializeFromStr, SerializeDisplay, PartialEq, Eq, Hash)]
236pub enum GlobalPermission {
237    Root,
238    Debug,
239    Demo,
240    SubscriptionsRead,
241    ArtefactsRead,
242    LegacyDialog,
243    SupportTenantAdmin,
244    SupportUsersWrite,
245    DeploymentQuotaWrite,
246    TenantAdmin,
247    TenantQuotaWrite,
248    Unknown(Box<str>),
249}
250
251const ROOT_AS_STR: &str = "root";
252const DEBUG_AS_STR: &str = "debug";
253const DEMO_AS_STR: &str = "demo";
254const SUBSCRIPTIONS_READ_AS_STR: &str = "subscriptions-read";
255const ARTEFACTS_READ_AS_STR: &str = "artefacts-read";
256const LEGACY_DIALOG_AS_STR: &str = "dialog";
257const SUPPORT_TENANT_ADMIN_AS_STR: &str = "support-tenant-admin";
258const SUPPORT_USERS_WRITE_AS_STR: &str = "support-users-write";
259const DEPLOYMENT_QUOTA_WRITE_AS_STR: &str = "deployment-quota-write";
260const TENANT_ADMIN_AS_STR: &str = "tenant-admin";
261const TENANT_QUOTA_WRITE_AS_STR: &str = "tenant-quota-write";
262
263impl FromStr for GlobalPermission {
264    type Err = Error;
265
266    fn from_str(string: &str) -> Result<Self> {
267        Ok(match string {
268            ROOT_AS_STR => GlobalPermission::Root,
269            DEBUG_AS_STR => GlobalPermission::Debug,
270            DEMO_AS_STR => GlobalPermission::Demo,
271            SUBSCRIPTIONS_READ_AS_STR => GlobalPermission::SubscriptionsRead,
272            ARTEFACTS_READ_AS_STR => GlobalPermission::ArtefactsRead,
273            LEGACY_DIALOG_AS_STR => GlobalPermission::LegacyDialog,
274            SUPPORT_TENANT_ADMIN_AS_STR => GlobalPermission::SupportTenantAdmin,
275            SUPPORT_USERS_WRITE_AS_STR => GlobalPermission::SupportUsersWrite,
276            TENANT_ADMIN_AS_STR => GlobalPermission::TenantAdmin,
277            TENANT_QUOTA_WRITE_AS_STR => GlobalPermission::TenantQuotaWrite,
278            DEPLOYMENT_QUOTA_WRITE_AS_STR => GlobalPermission::DeploymentQuotaWrite,
279            value => GlobalPermission::Unknown(value.into()),
280        })
281    }
282}
283
284impl Display for GlobalPermission {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(
287            f,
288            "{}",
289            match self {
290                GlobalPermission::Root => ROOT_AS_STR,
291                GlobalPermission::Debug => DEBUG_AS_STR,
292                GlobalPermission::Demo => DEMO_AS_STR,
293                GlobalPermission::SubscriptionsRead => SUBSCRIPTIONS_READ_AS_STR,
294                GlobalPermission::ArtefactsRead => ARTEFACTS_READ_AS_STR,
295                GlobalPermission::LegacyDialog => LEGACY_DIALOG_AS_STR,
296                GlobalPermission::SupportTenantAdmin => SUPPORT_TENANT_ADMIN_AS_STR,
297                GlobalPermission::SupportUsersWrite => SUPPORT_USERS_WRITE_AS_STR,
298                GlobalPermission::TenantAdmin => TENANT_ADMIN_AS_STR,
299                GlobalPermission::TenantQuotaWrite => TENANT_QUOTA_WRITE_AS_STR,
300                GlobalPermission::DeploymentQuotaWrite => DEPLOYMENT_QUOTA_WRITE_AS_STR,
301                GlobalPermission::Unknown(value) => value.as_ref(),
302            }
303        )
304    }
305}
306
307#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
308pub struct UpdateUser {
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub organisation_permissions: Option<HashMap<ProjectName, Vec<ProjectPermission>>>,
311
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub global_permissions: Option<Vec<GlobalPermission>>,
314}
315
316#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
317pub(crate) struct PostUserRequest<'request> {
318    pub user: &'request UpdateUser,
319}
320
321#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
322pub struct PostUserResponse {
323    pub user: User,
324}
325
326#[cfg(test)]
327mod tests {
328
329    use super::*;
330
331    #[test]
332    fn test_global_permissions_json_round_trip() {
333        let global_permissions = vec![
334            GlobalPermission::Root,
335            GlobalPermission::Debug,
336            GlobalPermission::Demo,
337            GlobalPermission::SubscriptionsRead,
338            GlobalPermission::ArtefactsRead,
339            GlobalPermission::LegacyDialog,
340            GlobalPermission::SupportTenantAdmin,
341            GlobalPermission::SupportUsersWrite,
342            GlobalPermission::TenantAdmin,
343            GlobalPermission::TenantQuotaWrite,
344            GlobalPermission::DeploymentQuotaWrite,
345            GlobalPermission::Unknown("new-perm".to_string().into_boxed_str()),
346        ];
347        let global_permissions_as_json_str = serde_json::to_string(&global_permissions).unwrap();
348
349        assert_eq!("[\"root\",\"debug\",\"demo\",\"subscriptions-read\",\"artefacts-read\",\"dialog\",\"support-tenant-admin\",\"support-users-write\",\"tenant-admin\",\"tenant-quota-write\",\"deployment-quota-write\",\"new-perm\"]", global_permissions_as_json_str);
350
351        let global_permissions_from_json_str: Vec<GlobalPermission> =
352            serde_json::from_str(&global_permissions_as_json_str).unwrap();
353
354        assert_eq!(global_permissions, global_permissions_from_json_str);
355    }
356    #[test]
357    fn unknown_project_permission_roundtrips() {
358        let unknown_permission = ProjectPermission::from_str("unknown").unwrap();
359        match &unknown_permission {
360            ProjectPermission::Unknown(error) => assert_eq!(&**error, "unknown"),
361            _ => panic!("Expected error to be parsed as Unknown(..)"),
362        }
363
364        assert_eq!(
365            &serde_json::ser::to_string(&unknown_permission).unwrap(),
366            "\"unknown\""
367        )
368    }
369
370    #[test]
371    #[should_panic]
372    fn specific_project_permission_roundtrips() {
373        // TODO(jcalero)[RE-978] This test was written to showcase bug RE-978. It demonstrates that
374        // deserialization of an `ProjectPermission` does not parse correctly.
375        let permission = ProjectPermission::DatasetsRead;
376
377        assert_eq!(
378            &serde_json::ser::to_string(&permission).unwrap(),
379            "\"voc-readonly\""
380        )
381    }
382
383    #[test]
384    fn unknown_global_permission_roundtrips() {
385        let unknown_permission = GlobalPermission::from_str("unknown").unwrap();
386        match &unknown_permission {
387            GlobalPermission::Unknown(error) => assert_eq!(&**error, "unknown"),
388            _ => panic!("Expected error to be parsed as Unknown(..)"),
389        }
390
391        assert_eq!(
392            &serde_json::ser::to_string(&unknown_permission).unwrap(),
393            "\"unknown\""
394        )
395    }
396}