1use std::fmt::Display;
4use std::str::FromStr;
5
6use nethsm_sdk_rs::apis::configuration::BasicAuth;
7use serde::{Deserialize, Serialize};
8use signstar_crypto::traits::UserWithPassphrase;
9use strum::AsRefStr;
10
11use crate::{Passphrase, UserRole};
12
13#[derive(Debug, thiserror::Error)]
15pub enum Error {
16 #[error("The passphrase for user {user} is missing")]
18 PassphraseMissing {
19 user: UserId,
21 },
22
23 #[error("Invalid Namespace IDs: {}", namespace_ids.join(", "))]
25 InvalidNamespaceIds {
26 namespace_ids: Vec<String>,
28 },
29
30 #[error("Invalid User IDs: {}", user_ids.join(", "))]
32 InvalidUserIds {
33 user_ids: Vec<String>,
35 },
36
37 #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
39 NamespaceUnsupported(UserId),
40
41 #[error("User {caller} targets {target} which is in a different namespace")]
43 NamespaceTargetMismatch {
44 caller: UserId,
46
47 target: UserId,
49 },
50
51 #[error("User {caller} targets {target} a system-wide user")]
53 NamespaceSystemWideTarget {
54 caller: UserId,
56
57 target: UserId,
59 },
60
61 #[error(
63 "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
64 )]
65 NamespaceRoleInvalid {
66 caller: UserId,
68
69 target: UserId,
71
72 role: UserRole,
74 },
75
76 #[error("The system-wide User ID has a namespace: {0}")]
78 SystemWideUserIdWithNamespace(UserId),
79}
80
81#[derive(AsRefStr, Clone, Debug, strum::Display, Eq, PartialEq)]
85#[strum(serialize_all = "lowercase")]
86pub enum NamespaceSupport {
87 Supported,
89 Unsupported,
91}
92
93#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
101pub struct NamespaceId(String);
102
103impl NamespaceId {
104 pub fn new(namespace_id: String) -> Result<Self, Error> {
128 if namespace_id.is_empty()
129 || !namespace_id.chars().all(|char| {
130 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
131 })
132 {
133 return Err(Error::InvalidNamespaceIds {
134 namespace_ids: vec![namespace_id],
135 });
136 }
137 Ok(Self(namespace_id))
138 }
139}
140
141impl AsRef<str> for NamespaceId {
142 fn as_ref(&self) -> &str {
143 self.0.as_str()
144 }
145}
146
147impl FromStr for NamespaceId {
148 type Err = Error;
149 fn from_str(s: &str) -> Result<Self, Self::Err> {
150 Self::new(s.to_string())
151 }
152}
153
154impl Display for NamespaceId {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "{}", self.0)
157 }
158}
159
160impl TryFrom<&str> for NamespaceId {
161 type Error = Error;
162
163 fn try_from(value: &str) -> Result<Self, Self::Error> {
164 Self::new(value.to_string())
165 }
166}
167
168#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
183#[serde(into = "String", try_from = "String")]
184pub enum UserId {
185 SystemWide(String),
187 Namespace(NamespaceId, String),
189}
190
191impl UserId {
192 pub fn new(user_id: String) -> Result<Self, Error> {
226 if let Some((namespace, name)) = user_id.split_once("~") {
227 if namespace.is_empty()
228 || !(namespace.chars().all(|char| {
229 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
230 }) && name.chars().all(|char| {
231 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
232 }))
233 {
234 return Err(Error::InvalidUserIds {
235 user_ids: vec![user_id],
236 });
237 }
238 Ok(Self::Namespace(namespace.parse()?, name.to_string()))
239 } else {
240 if user_id.is_empty()
241 || !user_id.chars().all(|char| {
242 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
243 })
244 {
245 return Err(Error::InvalidUserIds {
246 user_ids: vec![user_id],
247 });
248 }
249 Ok(Self::SystemWide(user_id))
250 }
251 }
252
253 pub fn namespace(&self) -> Option<&NamespaceId> {
272 match self {
273 Self::SystemWide(_) => None,
274 Self::Namespace(namespace, _) => Some(namespace),
275 }
276 }
277
278 pub fn is_namespaced(&self) -> bool {
297 match self {
298 Self::SystemWide(_) => false,
299 Self::Namespace(_, _) => true,
300 }
301 }
302
303 pub fn validate_namespace_access(
326 &self,
327 support: NamespaceSupport,
328 target: Option<&UserId>,
329 role: Option<&UserRole>,
330 ) -> Result<(), Error> {
331 if let Some(caller_namespace) = self.namespace() {
333 if support == NamespaceSupport::Unsupported {
335 return Err(Error::NamespaceUnsupported(self.to_owned()));
336 }
337
338 if let Some(target) = target {
340 if let Some(target_namespace) = target.namespace() {
342 if caller_namespace != target_namespace {
344 return Err(Error::NamespaceTargetMismatch {
345 caller: self.to_owned(),
346 target: target.to_owned(),
347 });
348 }
349
350 if let Some(role) = role {
352 if role == &UserRole::Metrics || role == &UserRole::Backup {
354 return Err(Error::NamespaceRoleInvalid {
355 caller: self.to_owned(),
356 target: target.to_owned(),
357 role: role.to_owned(),
358 });
359 }
360 }
361 } else {
362 return Err(Error::NamespaceSystemWideTarget {
364 caller: self.to_owned(),
365 target: target.to_owned(),
366 });
367 }
368 }
369 } else if let Some(target) = target {
371 if let Some(role) = role {
373 if (role == &UserRole::Metrics || role == &UserRole::Backup)
375 && target.is_namespaced()
376 {
377 return Err(Error::NamespaceRoleInvalid {
378 caller: self.to_owned(),
379 target: target.to_owned(),
380 role: role.to_owned(),
381 });
382 }
383 }
384 }
385 Ok(())
386 }
387}
388
389impl FromStr for UserId {
390 type Err = Error;
391 fn from_str(s: &str) -> Result<Self, Self::Err> {
392 Self::new(s.to_string())
393 }
394}
395
396impl Display for UserId {
397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 match self {
399 UserId::SystemWide(user_id) => write!(f, "{user_id}"),
400 UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
401 }
402 }
403}
404
405impl From<UserId> for String {
406 fn from(value: UserId) -> Self {
407 value.to_string()
408 }
409}
410
411impl TryFrom<&str> for UserId {
412 type Error = Error;
413
414 fn try_from(value: &str) -> Result<Self, Self::Error> {
415 Self::new(value.to_string())
416 }
417}
418
419impl TryFrom<&String> for UserId {
420 type Error = Error;
421
422 fn try_from(value: &String) -> Result<Self, Self::Error> {
423 Self::new(value.to_string())
424 }
425}
426
427impl TryFrom<String> for UserId {
428 type Error = Error;
429
430 fn try_from(value: String) -> Result<Self, Self::Error> {
431 Self::new(value)
432 }
433}
434
435#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
437#[serde(into = "String", try_from = "String")]
438pub struct SystemWideUserId(UserId);
439
440impl SystemWideUserId {
441 pub fn new(user_id: String) -> Result<Self, Error> {
462 let user_id = UserId::new(user_id)?;
463
464 if user_id.is_namespaced() {
465 return Err(Error::SystemWideUserIdWithNamespace(user_id));
466 }
467
468 Ok(Self(user_id))
469 }
470}
471
472impl AsRef<UserId> for SystemWideUserId {
473 fn as_ref(&self) -> &UserId {
474 &self.0
475 }
476}
477
478impl Display for SystemWideUserId {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 self.0.fmt(f)
481 }
482}
483
484impl FromStr for SystemWideUserId {
485 type Err = Error;
486 fn from_str(s: &str) -> Result<Self, Self::Err> {
487 Self::new(s.to_string())
488 }
489}
490
491impl From<SystemWideUserId> for String {
492 fn from(value: SystemWideUserId) -> Self {
493 value.to_string()
494 }
495}
496
497impl From<SystemWideUserId> for UserId {
498 fn from(value: SystemWideUserId) -> Self {
499 value.0
500 }
501}
502
503impl TryFrom<String> for SystemWideUserId {
504 type Error = Error;
505
506 fn try_from(value: String) -> Result<Self, Self::Error> {
507 Self::new(value)
508 }
509}
510
511#[derive(Clone, Debug, Deserialize, Serialize)]
516pub struct FullCredentials {
517 pub name: UserId,
519
520 pub passphrase: Passphrase,
522}
523
524impl FullCredentials {
525 pub fn new(name: UserId, passphrase: Passphrase) -> Self {
539 Self { name, passphrase }
540 }
541}
542
543impl UserWithPassphrase for FullCredentials {
544 fn user(&self) -> String {
545 self.name.to_string()
546 }
547
548 fn passphrase(&self) -> &Passphrase {
549 &self.passphrase
550 }
551}
552
553impl From<FullCredentials> for BasicAuth {
554 fn from(value: FullCredentials) -> Self {
555 Self::from(&value)
556 }
557}
558
559impl From<&FullCredentials> for BasicAuth {
560 fn from(value: &FullCredentials) -> Self {
561 (
562 value.name.to_string(),
563 Some(value.passphrase.expose_owned()),
564 )
565 }
566}
567
568impl TryFrom<&Credentials> for FullCredentials {
569 type Error = Error;
570
571 fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
572 let creds = value.clone();
573 FullCredentials::try_from(creds)
574 }
575}
576
577impl TryFrom<Credentials> for FullCredentials {
578 type Error = Error;
579
580 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
581 let Some(passphrase) = value.passphrase else {
582 return Err(Error::PassphraseMissing {
583 user: value.user_id,
584 });
585 };
586
587 Ok(FullCredentials {
588 name: value.user_id,
589 passphrase,
590 })
591 }
592}
593
594#[derive(Clone, Debug)]
598pub struct Credentials {
599 pub user_id: UserId,
601
602 pub passphrase: Option<Passphrase>,
604}
605
606impl Credentials {
607 pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
623 Self {
624 user_id,
625 passphrase,
626 }
627 }
628}
629
630impl Display for Credentials {
631 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632 write!(f, "{}", self.user_id)?;
633 if let Some(passphrase) = self.passphrase.as_ref() {
634 write!(f, " ({passphrase})")?;
635 }
636 Ok(())
637 }
638}
639
640impl From<Credentials> for BasicAuth {
641 fn from(value: Credentials) -> Self {
642 (
643 value.user_id.to_string(),
644 value.passphrase.map(|x| x.expose_owned()),
645 )
646 }
647}
648
649impl From<&Credentials> for BasicAuth {
650 fn from(value: &Credentials) -> Self {
651 (
652 value.user_id.to_string(),
653 value.passphrase.as_ref().map(|x| x.expose_owned()),
654 )
655 }
656}
657
658impl From<&FullCredentials> for Credentials {
659 fn from(value: &FullCredentials) -> Self {
660 let creds = value.clone();
661 Self::from(creds)
662 }
663}
664
665impl From<FullCredentials> for Credentials {
666 fn from(value: FullCredentials) -> Self {
667 Credentials::new(value.name, Some(value.passphrase))
668 }
669}
670
671impl TryFrom<Box<dyn UserWithPassphrase>> for Credentials {
672 type Error = crate::Error;
673
674 fn try_from(value: Box<dyn UserWithPassphrase>) -> Result<Self, Self::Error> {
675 Ok(Self::new(
676 UserId::try_from(value.user())?,
677 Some(value.passphrase().clone()),
678 ))
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use rstest::rstest;
685 use testresult::TestResult;
686
687 use super::*;
688
689 #[rstest]
690 #[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
691 #[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
692 fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
693 assert_eq!(credentials.to_string(), expected);
694 Ok(())
695 }
696
697 #[rstest]
698 #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
699 #[case("f", Some(UserId::SystemWide("f".to_string())))]
700 #[case("1", Some(UserId::SystemWide("1".to_string())))]
701 #[case("foo;-", None)]
702 #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
703 #[case("FOO", None)]
704 #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
705 #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
706 #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
707 #[case("~bar", None)]
708 #[case("", None)]
709 #[case("foo;-~bar\\", None)]
710 #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
711 #[case("foo~bar~baz", None)]
712 #[case("FOO~bar", None)]
713 #[case("foo~BAR", None)]
714 fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
715 if let Some(user_id) = user_id {
716 assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
717 } else {
718 assert!(UserId::from_str(input).is_err());
719 }
720
721 Ok(())
722 }
723
724 #[rstest]
725 #[case(UserId::SystemWide("user".to_string()), None)]
726 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
727 fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
728 assert_eq!(input.namespace(), result.as_ref());
729 Ok(())
730 }
731
732 #[rstest]
733 #[case(UserId::SystemWide("user".to_string()), false)]
734 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
735 fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
736 assert_eq!(input.is_namespaced(), result);
737 Ok(())
738 }
739
740 #[rstest]
741 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
742 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
743 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
744 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
745 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
746 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
747 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
748 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
749 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
750 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
751 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
752 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
753 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
754 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
755 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
756 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
757 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
758 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
759 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
760 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
761 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
762 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
763 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
764 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
765 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
766 fn validate_namespace_access(
767 #[case] caller: UserId,
768 #[case] namespace_support: NamespaceSupport,
769 #[case] target: Option<UserId>,
770 #[case] role: Option<UserRole>,
771 #[case] result: Option<()>,
772 ) -> TestResult {
773 if result.is_some() {
774 assert!(
775 caller
776 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
777 .is_ok()
778 );
779 } else {
780 assert!(
781 caller
782 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
783 .is_err()
784 )
785 }
786 Ok(())
787 }
788
789 #[test]
791 fn system_wide_user_id_new_fails_on_user_id_with_namespace() -> TestResult {
792 match SystemWideUserId::new("ns1~test".to_string()) {
793 Err(Error::SystemWideUserIdWithNamespace(_)) => Ok(()),
794 Err(error) => panic!(
795 "Expected to fail with a Error::SystemWideUserIdWithNamespace but got a different error instead:\n{error}"
796 ),
797 Ok(user_id) => panic!(
798 "Expected to fail with a Error::SystemWideUserIdWithNamespace but succeeded instead:\n{user_id}"
799 ),
800 }
801 }
802
803 #[test]
805 fn system_wide_user_id_new_fails_on_invalid_user_id() -> TestResult {
806 match SystemWideUserId::new("test[]".to_string()) {
807 Err(Error::InvalidUserIds { .. }) => Ok(()),
808 Err(error) => panic!(
809 "Expected to fail with a Error::InvalidUserIds but got a different error instead:\n{error}"
810 ),
811 Ok(user_id) => panic!(
812 "Expected to fail with a Error::InvalidUserIds but succeeded instead:\n{user_id}"
813 ),
814 }
815 }
816
817 #[test]
819 fn system_wide_user_id_from_str_succeeds() -> TestResult {
820 assert_eq!(SystemWideUserId::from_str("test")?.to_string(), "test");
821 Ok(())
822 }
823}