]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-api-types/src/userid.rs
update to first proxmox crate split
[proxmox-backup.git] / pbs-api-types / src / userid.rs
1 //! Types for user handling.
2 //!
3 //! We have [`Username`]s, [`Realm`]s and [`Tokenname`]s. To uniquely identify a user/API token, they
4 //! must be combined into a [`Userid`] or [`Authid`].
5 //!
6 //! Since they're all string types, they're organized as follows:
7 //!
8 //! * [`Username`]: an owned user name. Internally a `String`.
9 //! * [`UsernameRef`]: a borrowed user name. Pairs with a `Username` the same way a `str` pairs
10 //! with `String`, meaning you can only make references to it.
11 //! * [`Realm`]: an owned realm (`String` equivalent).
12 //! * [`RealmRef`]: a borrowed realm (`str` equivalent).
13 //! * [`Tokenname`]: an owned API token name (`String` equivalent)
14 //! * [`TokennameRef`]: a borrowed `Tokenname` (`str` equivalent).
15 //! * [`Userid`]: an owned user id (`"user@realm"`).
16 //! * [`Authid`]: an owned Authentication ID (a `Userid` with an optional `Tokenname`).
17 //! Note that `Userid` and `Authid` do not have a separate borrowed type.
18 //!
19 //! Note that `Username`s are not unique, therefore they do not implement `Eq` and cannot be
20 //! compared directly. If a direct comparison is really required, they can be compared as strings
21 //! via the `as_str()` method. [`Realm`]s, [`Userid`]s and [`Authid`]s on the other hand can be
22 //! compared with each other, as in those cases the comparison has meaning.
23
24 use std::borrow::Borrow;
25 use std::convert::TryFrom;
26 use std::fmt;
27
28 use anyhow::{bail, format_err, Error};
29 use lazy_static::lazy_static;
30 use serde::{Deserialize, Serialize};
31
32 use proxmox_schema::{
33 api, const_regex, ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType,
34 };
35
36 // we only allow a limited set of characters
37 // colon is not allowed, because we store usernames in
38 // colon separated lists)!
39 // slash is not allowed because it is used as pve API delimiter
40 // also see "man useradd"
41 #[macro_export]
42 macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") }
43 #[macro_export]
44 macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) }
45 #[macro_export]
46 macro_rules! TOKEN_NAME_REGEX_STR { () => (PROXMOX_SAFE_ID_REGEX_STR!()) }
47 #[macro_export]
48 macro_rules! USER_ID_REGEX_STR { () => (concat!(USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!())) }
49 #[macro_export]
50 macro_rules! APITOKEN_ID_REGEX_STR { () => (concat!(USER_ID_REGEX_STR!() , r"!", TOKEN_NAME_REGEX_STR!())) }
51
52 const_regex! {
53 pub PROXMOX_USER_NAME_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"$");
54 pub PROXMOX_TOKEN_NAME_REGEX = concat!(r"^", TOKEN_NAME_REGEX_STR!(), r"$");
55 pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_ID_REGEX_STR!(), r"$");
56 pub PROXMOX_APITOKEN_ID_REGEX = concat!(r"^", APITOKEN_ID_REGEX_STR!(), r"$");
57 pub PROXMOX_AUTH_ID_REGEX = concat!(r"^", r"(?:", USER_ID_REGEX_STR!(), r"|", APITOKEN_ID_REGEX_STR!(), r")$");
58 pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_REGEX_STR!(), r"$");
59 }
60
61 pub const PROXMOX_USER_NAME_FORMAT: ApiStringFormat =
62 ApiStringFormat::Pattern(&PROXMOX_USER_NAME_REGEX);
63 pub const PROXMOX_TOKEN_NAME_FORMAT: ApiStringFormat =
64 ApiStringFormat::Pattern(&PROXMOX_TOKEN_NAME_REGEX);
65
66 pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat =
67 ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX);
68 pub const PROXMOX_TOKEN_ID_FORMAT: ApiStringFormat =
69 ApiStringFormat::Pattern(&PROXMOX_APITOKEN_ID_REGEX);
70 pub const PROXMOX_AUTH_ID_FORMAT: ApiStringFormat =
71 ApiStringFormat::Pattern(&PROXMOX_AUTH_ID_REGEX);
72
73 pub const PROXMOX_TOKEN_ID_SCHEMA: Schema = StringSchema::new("API Token ID")
74 .format(&PROXMOX_TOKEN_ID_FORMAT)
75 .min_length(3)
76 .max_length(64)
77 .schema();
78
79 pub const PROXMOX_TOKEN_NAME_SCHEMA: Schema = StringSchema::new("API Token name")
80 .format(&PROXMOX_TOKEN_NAME_FORMAT)
81 .min_length(3)
82 .max_length(64)
83 .schema();
84
85 pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat =
86 ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX);
87
88 pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID")
89 .format(&PROXMOX_GROUP_ID_FORMAT)
90 .min_length(3)
91 .max_length(64)
92 .schema();
93
94 pub const PROXMOX_AUTH_REALM_STRING_SCHEMA: StringSchema =
95 StringSchema::new("Authentication domain ID")
96 .format(&super::PROXMOX_SAFE_ID_FORMAT)
97 .min_length(3)
98 .max_length(32);
99 pub const PROXMOX_AUTH_REALM_SCHEMA: Schema = PROXMOX_AUTH_REALM_STRING_SCHEMA.schema();
100
101 #[api(
102 type: String,
103 format: &PROXMOX_USER_NAME_FORMAT,
104 )]
105 /// The user name part of a user id.
106 ///
107 /// This alone does NOT uniquely identify the user and therefore does not implement `Eq`. In order
108 /// to compare user names directly, they need to be explicitly compared as strings by calling
109 /// `.as_str()`.
110 ///
111 /// ```compile_fail
112 /// fn test(a: Username, b: Username) -> bool {
113 /// a == b // illegal and does not compile
114 /// }
115 /// ```
116 #[derive(Clone, Debug, Hash, Deserialize, Serialize)]
117 pub struct Username(String);
118
119 /// A reference to a user name part of a user id. This alone does NOT uniquely identify the user.
120 ///
121 /// This is like a `str` to the `String` of a [`Username`].
122 #[derive(Debug, Hash)]
123 pub struct UsernameRef(str);
124
125 impl UsernameRef {
126 fn new(s: &str) -> &Self {
127 unsafe { &*(s as *const str as *const UsernameRef) }
128 }
129
130 pub fn as_str(&self) -> &str {
131 &self.0
132 }
133 }
134
135 impl std::ops::Deref for Username {
136 type Target = UsernameRef;
137
138 fn deref(&self) -> &UsernameRef {
139 self.borrow()
140 }
141 }
142
143 impl Borrow<UsernameRef> for Username {
144 fn borrow(&self) -> &UsernameRef {
145 UsernameRef::new(self.0.as_str())
146 }
147 }
148
149 impl AsRef<UsernameRef> for Username {
150 fn as_ref(&self) -> &UsernameRef {
151 self.borrow()
152 }
153 }
154
155 impl ToOwned for UsernameRef {
156 type Owned = Username;
157
158 fn to_owned(&self) -> Self::Owned {
159 Username(self.0.to_owned())
160 }
161 }
162
163 impl TryFrom<String> for Username {
164 type Error = Error;
165
166 fn try_from(s: String) -> Result<Self, Error> {
167 if !PROXMOX_USER_NAME_REGEX.is_match(&s) {
168 bail!("invalid user name");
169 }
170
171 Ok(Self(s))
172 }
173 }
174
175 impl<'a> TryFrom<&'a str> for &'a UsernameRef {
176 type Error = Error;
177
178 fn try_from(s: &'a str) -> Result<&'a UsernameRef, Error> {
179 if !PROXMOX_USER_NAME_REGEX.is_match(s) {
180 bail!("invalid name in user id");
181 }
182
183 Ok(UsernameRef::new(s))
184 }
185 }
186
187 #[api(schema: PROXMOX_AUTH_REALM_SCHEMA)]
188 /// An authentication realm.
189 #[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
190 pub struct Realm(String);
191
192 /// A reference to an authentication realm.
193 ///
194 /// This is like a `str` to the `String` of a `Realm`.
195 #[derive(Debug, Hash, Eq, PartialEq)]
196 pub struct RealmRef(str);
197
198 impl RealmRef {
199 fn new(s: &str) -> &Self {
200 unsafe { &*(s as *const str as *const RealmRef) }
201 }
202
203 pub fn as_str(&self) -> &str {
204 &self.0
205 }
206 }
207
208 impl std::ops::Deref for Realm {
209 type Target = RealmRef;
210
211 fn deref(&self) -> &RealmRef {
212 self.borrow()
213 }
214 }
215
216 impl Borrow<RealmRef> for Realm {
217 fn borrow(&self) -> &RealmRef {
218 RealmRef::new(self.0.as_str())
219 }
220 }
221
222 impl AsRef<RealmRef> for Realm {
223 fn as_ref(&self) -> &RealmRef {
224 self.borrow()
225 }
226 }
227
228 impl ToOwned for RealmRef {
229 type Owned = Realm;
230
231 fn to_owned(&self) -> Self::Owned {
232 Realm(self.0.to_owned())
233 }
234 }
235
236 impl TryFrom<String> for Realm {
237 type Error = Error;
238
239 fn try_from(s: String) -> Result<Self, Error> {
240 PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(&s)
241 .map_err(|_| format_err!("invalid realm"))?;
242
243 Ok(Self(s))
244 }
245 }
246
247 impl<'a> TryFrom<&'a str> for &'a RealmRef {
248 type Error = Error;
249
250 fn try_from(s: &'a str) -> Result<&'a RealmRef, Error> {
251 PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(s)
252 .map_err(|_| format_err!("invalid realm"))?;
253
254 Ok(RealmRef::new(s))
255 }
256 }
257
258 impl PartialEq<str> for Realm {
259 fn eq(&self, rhs: &str) -> bool {
260 self.0 == rhs
261 }
262 }
263
264 impl PartialEq<&str> for Realm {
265 fn eq(&self, rhs: &&str) -> bool {
266 self.0 == *rhs
267 }
268 }
269
270 impl PartialEq<str> for RealmRef {
271 fn eq(&self, rhs: &str) -> bool {
272 self.0 == *rhs
273 }
274 }
275
276 impl PartialEq<&str> for RealmRef {
277 fn eq(&self, rhs: &&str) -> bool {
278 self.0 == **rhs
279 }
280 }
281
282 impl PartialEq<RealmRef> for Realm {
283 fn eq(&self, rhs: &RealmRef) -> bool {
284 self.0 == rhs.0
285 }
286 }
287
288 impl PartialEq<Realm> for RealmRef {
289 fn eq(&self, rhs: &Realm) -> bool {
290 self.0 == rhs.0
291 }
292 }
293
294 impl PartialEq<Realm> for &RealmRef {
295 fn eq(&self, rhs: &Realm) -> bool {
296 (*self).0 == rhs.0
297 }
298 }
299
300 #[api(
301 type: String,
302 format: &PROXMOX_TOKEN_NAME_FORMAT,
303 )]
304 /// The token ID part of an API token authentication id.
305 ///
306 /// This alone does NOT uniquely identify the API token - use a full `Authid` for such use cases.
307 #[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
308 pub struct Tokenname(String);
309
310 /// A reference to a token name part of an authentication id. This alone does NOT uniquely identify
311 /// the user.
312 ///
313 /// This is like a `str` to the `String` of a [`Tokenname`].
314 #[derive(Debug, Hash)]
315 pub struct TokennameRef(str);
316
317 #[doc(hidden)]
318 /// ```compile_fail
319 /// let a: Username = unsafe { std::mem::zeroed() };
320 /// let b: Username = unsafe { std::mem::zeroed() };
321 /// let _ = <Username as PartialEq>::eq(&a, &b);
322 /// ```
323 ///
324 /// ```compile_fail
325 /// let a: &UsernameRef = unsafe { std::mem::zeroed() };
326 /// let b: &UsernameRef = unsafe { std::mem::zeroed() };
327 /// let _ = <&UsernameRef as PartialEq>::eq(a, b);
328 /// ```
329 ///
330 /// ```compile_fail
331 /// let a: &UsernameRef = unsafe { std::mem::zeroed() };
332 /// let b: &UsernameRef = unsafe { std::mem::zeroed() };
333 /// let _ = <&UsernameRef as PartialEq>::eq(&a, &b);
334 /// ```
335 struct _AssertNoEqImpl;
336
337 impl TokennameRef {
338 fn new(s: &str) -> &Self {
339 unsafe { &*(s as *const str as *const TokennameRef) }
340 }
341
342 pub fn as_str(&self) -> &str {
343 &self.0
344 }
345 }
346
347 impl std::ops::Deref for Tokenname {
348 type Target = TokennameRef;
349
350 fn deref(&self) -> &TokennameRef {
351 self.borrow()
352 }
353 }
354
355 impl Borrow<TokennameRef> for Tokenname {
356 fn borrow(&self) -> &TokennameRef {
357 TokennameRef::new(self.0.as_str())
358 }
359 }
360
361 impl AsRef<TokennameRef> for Tokenname {
362 fn as_ref(&self) -> &TokennameRef {
363 self.borrow()
364 }
365 }
366
367 impl ToOwned for TokennameRef {
368 type Owned = Tokenname;
369
370 fn to_owned(&self) -> Self::Owned {
371 Tokenname(self.0.to_owned())
372 }
373 }
374
375 impl TryFrom<String> for Tokenname {
376 type Error = Error;
377
378 fn try_from(s: String) -> Result<Self, Error> {
379 if !PROXMOX_TOKEN_NAME_REGEX.is_match(&s) {
380 bail!("invalid token name");
381 }
382
383 Ok(Self(s))
384 }
385 }
386
387 impl<'a> TryFrom<&'a str> for &'a TokennameRef {
388 type Error = Error;
389
390 fn try_from(s: &'a str) -> Result<&'a TokennameRef, Error> {
391 if !PROXMOX_TOKEN_NAME_REGEX.is_match(s) {
392 bail!("invalid token name in user id");
393 }
394
395 Ok(TokennameRef::new(s))
396 }
397 }
398
399 /// A complete user id consisting of a user name and a realm
400 #[derive(Clone, Debug, PartialEq, Eq, Hash, UpdaterType)]
401 pub struct Userid {
402 data: String,
403 name_len: usize,
404 }
405
406 impl ApiType for Userid {
407 const API_SCHEMA: Schema = StringSchema::new("User ID")
408 .format(&PROXMOX_USER_ID_FORMAT)
409 .min_length(3)
410 .max_length(64)
411 .schema();
412 }
413
414 impl Userid {
415 const fn new(data: String, name_len: usize) -> Self {
416 Self { data, name_len }
417 }
418
419 pub fn name(&self) -> &UsernameRef {
420 UsernameRef::new(&self.data[..self.name_len])
421 }
422
423 pub fn realm(&self) -> &RealmRef {
424 RealmRef::new(&self.data[(self.name_len + 1)..])
425 }
426
427 pub fn as_str(&self) -> &str {
428 &self.data
429 }
430
431 /// Get the "root@pam" user id.
432 pub fn root_userid() -> &'static Self {
433 &*ROOT_USERID
434 }
435 }
436
437 lazy_static! {
438 pub static ref ROOT_USERID: Userid = Userid::new("root@pam".to_string(), 4);
439 }
440
441 impl From<Authid> for Userid {
442 fn from(authid: Authid) -> Self {
443 authid.user
444 }
445 }
446
447 impl From<(Username, Realm)> for Userid {
448 fn from(parts: (Username, Realm)) -> Self {
449 Self::from((parts.0.as_ref(), parts.1.as_ref()))
450 }
451 }
452
453 impl From<(&UsernameRef, &RealmRef)> for Userid {
454 fn from(parts: (&UsernameRef, &RealmRef)) -> Self {
455 let data = format!("{}@{}", parts.0.as_str(), parts.1.as_str());
456 let name_len = parts.0.as_str().len();
457 Self { data, name_len }
458 }
459 }
460
461 impl fmt::Display for Userid {
462 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
463 self.data.fmt(f)
464 }
465 }
466
467 impl std::str::FromStr for Userid {
468 type Err = Error;
469
470 fn from_str(id: &str) -> Result<Self, Error> {
471 let name_len = id
472 .as_bytes()
473 .iter()
474 .rposition(|&b| b == b'@')
475 .ok_or_else(|| format_err!("not a valid user id"))?;
476
477 let name = &id[..name_len];
478 let realm = &id[(name_len + 1)..];
479
480 if !PROXMOX_USER_NAME_REGEX.is_match(name) {
481 bail!("invalid user name in user id");
482 }
483
484 PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(realm)
485 .map_err(|_| format_err!("invalid realm in user id"))?;
486
487 Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm))))
488 }
489 }
490
491 impl TryFrom<String> for Userid {
492 type Error = Error;
493
494 fn try_from(data: String) -> Result<Self, Error> {
495 let name_len = data
496 .as_bytes()
497 .iter()
498 .rposition(|&b| b == b'@')
499 .ok_or_else(|| format_err!("not a valid user id"))?;
500
501 if !PROXMOX_USER_NAME_REGEX.is_match(&data[..name_len]) {
502 bail!("invalid user name in user id");
503 }
504
505 PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(&data[(name_len + 1)..])
506 .map_err(|_| format_err!("invalid realm in user id"))?;
507
508 Ok(Self { data, name_len })
509 }
510 }
511
512 impl PartialEq<str> for Userid {
513 fn eq(&self, rhs: &str) -> bool {
514 self.data == *rhs
515 }
516 }
517
518 impl PartialEq<&str> for Userid {
519 fn eq(&self, rhs: &&str) -> bool {
520 *self == **rhs
521 }
522 }
523
524 impl PartialEq<String> for Userid {
525 fn eq(&self, rhs: &String) -> bool {
526 self == rhs.as_str()
527 }
528 }
529
530 /// A complete authentication id consisting of a user id and an optional token name.
531 #[derive(Clone, Debug, Eq, PartialEq, Hash, UpdaterType)]
532 pub struct Authid {
533 user: Userid,
534 tokenname: Option<Tokenname>
535 }
536
537 impl ApiType for Authid {
538 const API_SCHEMA: Schema = StringSchema::new("Authentication ID")
539 .format(&PROXMOX_AUTH_ID_FORMAT)
540 .min_length(3)
541 .max_length(64)
542 .schema();
543 }
544
545 impl Authid {
546 const fn new(user: Userid, tokenname: Option<Tokenname>) -> Self {
547 Self { user, tokenname }
548 }
549
550 pub fn user(&self) -> &Userid {
551 &self.user
552 }
553
554 pub fn is_token(&self) -> bool {
555 self.tokenname.is_some()
556 }
557
558 pub fn tokenname(&self) -> Option<&TokennameRef> {
559 match &self.tokenname {
560 Some(name) => Some(&name),
561 None => None,
562 }
563 }
564
565 /// Get the "root@pam" auth id.
566 pub fn root_auth_id() -> &'static Self {
567 &*ROOT_AUTHID
568 }
569 }
570
571 lazy_static! {
572 pub static ref ROOT_AUTHID: Authid = Authid::from(Userid::new("root@pam".to_string(), 4));
573 }
574
575 impl From<Userid> for Authid {
576 fn from(parts: Userid) -> Self {
577 Self::new(parts, None)
578 }
579 }
580
581 impl From<(Userid, Option<Tokenname>)> for Authid {
582 fn from(parts: (Userid, Option<Tokenname>)) -> Self {
583 Self::new(parts.0, parts.1)
584 }
585 }
586
587 impl fmt::Display for Authid {
588 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
589 match &self.tokenname {
590 Some(token) => write!(f, "{}!{}", self.user, token.as_str()),
591 None => self.user.fmt(f),
592 }
593 }
594 }
595
596 impl std::str::FromStr for Authid {
597 type Err = Error;
598
599 fn from_str(id: &str) -> Result<Self, Error> {
600 let name_len = id
601 .as_bytes()
602 .iter()
603 .rposition(|&b| b == b'@')
604 .ok_or_else(|| format_err!("not a valid user id"))?;
605
606 let realm_end = id
607 .as_bytes()
608 .iter()
609 .rposition(|&b| b == b'!')
610 .map(|pos| if pos < name_len { id.len() } else { pos })
611 .unwrap_or_else(|| id.len());
612
613 if realm_end == id.len() - 1 {
614 bail!("empty token name in userid");
615 }
616
617 let user = Userid::from_str(&id[..realm_end])?;
618
619 if id.len() > realm_end {
620 let token = Tokenname::try_from(id[(realm_end + 1)..].to_string())?;
621 Ok(Self::new(user, Some(token)))
622 } else {
623 Ok(Self::new(user, None))
624 }
625 }
626 }
627
628 impl TryFrom<String> for Authid {
629 type Error = Error;
630
631 fn try_from(mut data: String) -> Result<Self, Error> {
632 let name_len = data
633 .as_bytes()
634 .iter()
635 .rposition(|&b| b == b'@')
636 .ok_or_else(|| format_err!("not a valid user id"))?;
637
638 let realm_end = data
639 .as_bytes()
640 .iter()
641 .rposition(|&b| b == b'!')
642 .map(|pos| if pos < name_len { data.len() } else { pos })
643 .unwrap_or_else(|| data.len());
644
645 if realm_end == data.len() - 1 {
646 bail!("empty token name in userid");
647 }
648
649 let tokenname = if data.len() > realm_end {
650 Some(Tokenname::try_from(data[(realm_end + 1)..].to_string())?)
651 } else {
652 None
653 };
654
655 data.truncate(realm_end);
656
657 let user:Userid = data.parse()?;
658
659 Ok(Self { user, tokenname })
660 }
661 }
662
663 #[test]
664 fn test_token_id() {
665 let userid: Userid = "test@pam".parse().expect("parsing Userid failed");
666 assert_eq!(userid.name().as_str(), "test");
667 assert_eq!(userid.realm(), "pam");
668 assert_eq!(userid, "test@pam");
669
670 let auth_id: Authid = "test@pam".parse().expect("parsing user Authid failed");
671 assert_eq!(auth_id.to_string(), "test@pam".to_string());
672 assert!(!auth_id.is_token());
673
674 assert_eq!(auth_id.user(), &userid);
675
676 let user_auth_id = Authid::from(userid.clone());
677 assert_eq!(user_auth_id, auth_id);
678 assert!(!user_auth_id.is_token());
679
680 let auth_id: Authid = "test@pam!bar".parse().expect("parsing token Authid failed");
681 let token_userid = auth_id.user();
682 assert_eq!(&userid, token_userid);
683 assert!(auth_id.is_token());
684 assert_eq!(auth_id.tokenname().expect("Token has tokenname").as_str(), TokennameRef::new("bar").as_str());
685 assert_eq!(auth_id.to_string(), "test@pam!bar".to_string());
686 }
687
688 proxmox::forward_deserialize_to_from_str!(Userid);
689 proxmox::forward_serialize_to_display!(Userid);
690
691 proxmox::forward_deserialize_to_from_str!(Authid);
692 proxmox::forward_serialize_to_display!(Authid);