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 #[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 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}