1use std::fmt::Display;
4use std::str::FromStr;
5
6use nethsm_sdk_rs::apis::configuration::BasicAuth;
7use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize};
9
10use crate::UserRole;
11
12#[derive(Debug, thiserror::Error)]
14pub enum Error {
15 #[error("Unable to convert string to passphrase")]
17 Passphrase,
18
19 #[error("The passphrase for user {user} is missing")]
21 PassphraseMissing {
22 user: UserId,
24 },
25
26 #[error("Invalid Namespace ID: {0}")]
28 InvalidNamespaceId(String),
29
30 #[error("Invalid User ID: {0}")]
32 InvalidUserId(String),
33
34 #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
36 NamespaceUnsupported(UserId),
37
38 #[error("User {caller} targets {target} which is in a different namespace")]
40 NamespaceTargetMismatch {
41 caller: UserId,
43
44 target: UserId,
46 },
47
48 #[error("User {caller} targets {target} a system-wide user")]
50 NamespaceSystemWideTarget {
51 caller: UserId,
53
54 target: UserId,
56 },
57
58 #[error(
60 "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
61 )]
62 NamespaceRoleInvalid {
63 caller: UserId,
65
66 target: UserId,
68
69 role: UserRole,
71 },
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum NamespaceSupport {
79 Supported,
81 Unsupported,
83}
84
85#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
93pub struct NamespaceId(String);
94
95impl NamespaceId {
96 pub fn new(namespace_id: String) -> Result<Self, Error> {
120 if namespace_id.is_empty()
121 || !namespace_id.chars().all(|char| {
122 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
123 })
124 {
125 return Err(Error::InvalidNamespaceId(namespace_id));
126 }
127 Ok(Self(namespace_id))
128 }
129}
130
131impl AsRef<str> for NamespaceId {
132 fn as_ref(&self) -> &str {
133 self.0.as_str()
134 }
135}
136
137impl FromStr for NamespaceId {
138 type Err = Error;
139 fn from_str(s: &str) -> Result<Self, Self::Err> {
140 Self::new(s.to_string())
141 }
142}
143
144impl Display for NamespaceId {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{}", self.0)
147 }
148}
149
150impl TryFrom<&str> for NamespaceId {
151 type Error = Error;
152
153 fn try_from(value: &str) -> Result<Self, Self::Error> {
154 Self::new(value.to_string())
155 }
156}
157
158#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
173#[serde(into = "String", try_from = "String")]
174pub enum UserId {
175 SystemWide(String),
177 Namespace(NamespaceId, String),
179}
180
181impl UserId {
182 pub fn new(user_id: String) -> Result<Self, Error> {
216 if let Some((namespace, name)) = user_id.split_once("~") {
217 if namespace.is_empty()
218 || !(namespace.chars().all(|char| {
219 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
220 }) && name.chars().all(|char| {
221 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
222 }))
223 {
224 return Err(Error::InvalidUserId(user_id));
225 }
226 Ok(Self::Namespace(namespace.parse()?, name.to_string()))
227 } else {
228 if user_id.is_empty()
229 || !user_id.chars().all(|char| {
230 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
231 })
232 {
233 return Err(Error::InvalidUserId(user_id));
234 }
235 Ok(Self::SystemWide(user_id))
236 }
237 }
238
239 pub fn namespace(&self) -> Option<String> {
258 match self {
259 Self::SystemWide(_) => None,
260 Self::Namespace(namespace, _) => Some(namespace.to_string()),
261 }
262 }
263
264 pub fn is_namespaced(&self) -> bool {
283 match self {
284 Self::SystemWide(_) => false,
285 Self::Namespace(_, _) => true,
286 }
287 }
288
289 pub fn validate_namespace_access(
312 &self,
313 support: NamespaceSupport,
314 target: Option<&UserId>,
315 role: Option<&UserRole>,
316 ) -> Result<(), Error> {
317 if let Some(caller_namespace) = self.namespace() {
319 if support == NamespaceSupport::Unsupported {
321 return Err(Error::NamespaceUnsupported(self.to_owned()));
322 }
323
324 if let Some(target) = target {
326 if let Some(target_namespace) = target.namespace() {
328 if caller_namespace != target_namespace {
330 return Err(Error::NamespaceTargetMismatch {
331 caller: self.to_owned(),
332 target: target.to_owned(),
333 });
334 }
335
336 if let Some(role) = role {
338 if role == &UserRole::Metrics || role == &UserRole::Backup {
340 return Err(Error::NamespaceRoleInvalid {
341 caller: self.to_owned(),
342 target: target.to_owned(),
343 role: role.to_owned(),
344 });
345 }
346 }
347 } else {
348 return Err(Error::NamespaceSystemWideTarget {
350 caller: self.to_owned(),
351 target: target.to_owned(),
352 });
353 }
354 }
355 } else if let Some(target) = target {
357 if let Some(role) = role {
359 if (role == &UserRole::Metrics || role == &UserRole::Backup)
361 && target.is_namespaced()
362 {
363 return Err(Error::NamespaceRoleInvalid {
364 caller: self.to_owned(),
365 target: target.to_owned(),
366 role: role.to_owned(),
367 });
368 }
369 }
370 }
371 Ok(())
372 }
373}
374
375impl FromStr for UserId {
376 type Err = Error;
377 fn from_str(s: &str) -> Result<Self, Self::Err> {
378 Self::new(s.to_string())
379 }
380}
381
382impl Display for UserId {
383 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384 match self {
385 UserId::SystemWide(user_id) => write!(f, "{user_id}"),
386 UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
387 }
388 }
389}
390
391impl From<UserId> for String {
392 fn from(value: UserId) -> Self {
393 value.to_string()
394 }
395}
396
397impl TryFrom<&str> for UserId {
398 type Error = Error;
399
400 fn try_from(value: &str) -> Result<Self, Self::Error> {
401 Self::new(value.to_string())
402 }
403}
404
405impl TryFrom<&String> for UserId {
406 type Error = Error;
407
408 fn try_from(value: &String) -> Result<Self, Self::Error> {
409 Self::new(value.to_string())
410 }
411}
412
413impl TryFrom<String> for UserId {
414 type Error = Error;
415
416 fn try_from(value: String) -> Result<Self, Self::Error> {
417 Self::new(value)
418 }
419}
420
421#[derive(Clone, Debug, Deserialize, Serialize)]
426pub struct FullCredentials {
427 pub name: UserId,
429
430 pub passphrase: Passphrase,
432}
433
434impl FullCredentials {
435 pub fn new(name: UserId, passphrase: Passphrase) -> Self {
449 Self { name, passphrase }
450 }
451}
452
453impl From<FullCredentials> for BasicAuth {
454 fn from(value: FullCredentials) -> Self {
455 Self::from(&value)
456 }
457}
458
459impl From<&FullCredentials> for BasicAuth {
460 fn from(value: &FullCredentials) -> Self {
461 (
462 value.name.to_string(),
463 Some(value.passphrase.expose_owned()),
464 )
465 }
466}
467
468impl TryFrom<&Credentials> for FullCredentials {
469 type Error = Error;
470
471 fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
472 let creds = value.clone();
473 FullCredentials::try_from(creds)
474 }
475}
476
477impl TryFrom<Credentials> for FullCredentials {
478 type Error = Error;
479
480 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
481 let Some(passphrase) = value.passphrase else {
482 return Err(Error::PassphraseMissing {
483 user: value.user_id,
484 });
485 };
486
487 Ok(FullCredentials {
488 name: value.user_id,
489 passphrase,
490 })
491 }
492}
493
494#[derive(Clone, Debug)]
498pub struct Credentials {
499 pub user_id: UserId,
501
502 pub passphrase: Option<Passphrase>,
504}
505
506impl Credentials {
507 pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
523 Self {
524 user_id,
525 passphrase,
526 }
527 }
528}
529
530impl From<Credentials> for BasicAuth {
531 fn from(value: Credentials) -> Self {
532 (
533 value.user_id.to_string(),
534 value.passphrase.map(|x| x.expose_owned()),
535 )
536 }
537}
538
539impl From<&Credentials> for BasicAuth {
540 fn from(value: &Credentials) -> Self {
541 (
542 value.user_id.to_string(),
543 value.passphrase.as_ref().map(|x| x.expose_owned()),
544 )
545 }
546}
547
548impl From<&FullCredentials> for Credentials {
549 fn from(value: &FullCredentials) -> Self {
550 let creds = value.clone();
551 Self::from(creds)
552 }
553}
554
555impl From<FullCredentials> for Credentials {
556 fn from(value: FullCredentials) -> Self {
557 Credentials::new(value.name, Some(value.passphrase))
558 }
559}
560
561#[derive(Clone, Debug, Default, Deserialize)]
566pub struct Passphrase(SecretString);
567
568impl Passphrase {
569 pub fn new(passphrase: String) -> Self {
578 Self(SecretString::new(passphrase.into()))
579 }
580
581 pub fn expose_owned(&self) -> String {
586 self.0.expose_secret().to_owned()
587 }
588
589 pub fn expose_borrowed(&self) -> &str {
591 self.0.expose_secret()
592 }
593}
594
595impl FromStr for Passphrase {
596 type Err = Error;
597
598 fn from_str(s: &str) -> Result<Self, Self::Err> {
599 Ok(Self(SecretString::from(s.to_string())))
600 }
601}
602
603impl Serialize for Passphrase {
604 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
611 where
612 S: serde::Serializer,
613 {
614 self.0.expose_secret().serialize(serializer)
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use rstest::rstest;
621 use testresult::TestResult;
622
623 use super::*;
624
625 #[rstest]
626 #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
627 #[case("f", Some(UserId::SystemWide("f".to_string())))]
628 #[case("1", Some(UserId::SystemWide("1".to_string())))]
629 #[case("foo;-", None)]
630 #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
631 #[case("FOO", None)]
632 #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
633 #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
634 #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
635 #[case("~bar", None)]
636 #[case("", None)]
637 #[case("foo;-~bar\\", None)]
638 #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
639 #[case("foo~bar~baz", None)]
640 #[case("FOO~bar", None)]
641 #[case("foo~BAR", None)]
642 fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
643 if let Some(user_id) = user_id {
644 assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
645 } else {
646 assert!(UserId::from_str(input).is_err());
647 }
648
649 Ok(())
650 }
651
652 #[rstest]
653 #[case(UserId::SystemWide("user".to_string()), None)]
654 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some("namespace".to_string()))]
655 fn user_id_namespace(#[case] input: UserId, #[case] result: Option<String>) -> TestResult {
656 assert_eq!(input.namespace(), result);
657 Ok(())
658 }
659
660 #[rstest]
661 #[case(UserId::SystemWide("user".to_string()), false)]
662 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
663 fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
664 assert_eq!(input.is_namespaced(), result);
665 Ok(())
666 }
667
668 #[rstest]
669 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
670 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
671 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
672 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
673 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
674 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
675 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
676 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
677 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
678 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
679 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
680 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
681 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
682 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
683 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
684 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
685 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
686 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
687 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
688 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
689 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
690 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
691 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
692 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
693 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
694 fn validate_namespace_access(
695 #[case] caller: UserId,
696 #[case] namespace_support: NamespaceSupport,
697 #[case] target: Option<UserId>,
698 #[case] role: Option<UserRole>,
699 #[case] result: Option<()>,
700 ) -> TestResult {
701 if result.is_some() {
702 assert!(
703 caller
704 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
705 .is_ok()
706 );
707 } else {
708 assert!(
709 caller
710 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
711 .is_err()
712 )
713 }
714 Ok(())
715 }
716}