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 namespaced User ID has no namespace: {0}")]
78 NamespacedUserIdWithoutNamespace(UserId),
79
80 #[error("The system-wide User ID has a namespace: {0}")]
82 SystemWideUserIdWithNamespace(UserId),
83}
84
85#[derive(AsRefStr, Clone, Debug, strum::Display, Eq, PartialEq)]
89#[strum(serialize_all = "lowercase")]
90pub enum NamespaceSupport {
91 Supported,
93 Unsupported,
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
105pub struct NamespaceId(String);
106
107impl NamespaceId {
108 pub fn new(namespace_id: String) -> Result<Self, Error> {
132 if namespace_id.is_empty()
133 || !namespace_id.chars().all(|char| {
134 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
135 })
136 {
137 return Err(Error::InvalidNamespaceIds {
138 namespace_ids: vec![namespace_id],
139 });
140 }
141 Ok(Self(namespace_id))
142 }
143}
144
145impl AsRef<str> for NamespaceId {
146 fn as_ref(&self) -> &str {
147 self.0.as_str()
148 }
149}
150
151impl FromStr for NamespaceId {
152 type Err = Error;
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 Self::new(s.to_string())
155 }
156}
157
158impl Display for NamespaceId {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 write!(f, "{}", self.0)
161 }
162}
163
164impl TryFrom<&str> for NamespaceId {
165 type Error = Error;
166
167 fn try_from(value: &str) -> Result<Self, Self::Error> {
168 Self::new(value.to_string())
169 }
170}
171
172#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
187#[serde(into = "String", try_from = "String")]
188pub enum UserId {
189 SystemWide(String),
191 Namespace(NamespaceId, String),
193}
194
195impl UserId {
196 pub fn new(user_id: String) -> Result<Self, Error> {
230 if let Some((namespace, name)) = user_id.split_once("~") {
231 if namespace.is_empty()
232 || !(namespace.chars().all(|char| {
233 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
234 }) && name.chars().all(|char| {
235 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
236 }))
237 {
238 return Err(Error::InvalidUserIds {
239 user_ids: vec![user_id],
240 });
241 }
242 Ok(Self::Namespace(namespace.parse()?, name.to_string()))
243 } else {
244 if user_id.is_empty()
245 || !user_id.chars().all(|char| {
246 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
247 })
248 {
249 return Err(Error::InvalidUserIds {
250 user_ids: vec![user_id],
251 });
252 }
253 Ok(Self::SystemWide(user_id))
254 }
255 }
256
257 pub fn namespace(&self) -> Option<&NamespaceId> {
276 match self {
277 Self::SystemWide(_) => None,
278 Self::Namespace(namespace, _) => Some(namespace),
279 }
280 }
281
282 pub fn is_namespaced(&self) -> bool {
301 match self {
302 Self::SystemWide(_) => false,
303 Self::Namespace(_, _) => true,
304 }
305 }
306
307 pub fn validate_namespace_access(
330 &self,
331 support: NamespaceSupport,
332 target: Option<&UserId>,
333 role: Option<&UserRole>,
334 ) -> Result<(), Error> {
335 if let Some(caller_namespace) = self.namespace() {
337 if support == NamespaceSupport::Unsupported {
339 return Err(Error::NamespaceUnsupported(self.to_owned()));
340 }
341
342 if let Some(target) = target {
344 if let Some(target_namespace) = target.namespace() {
346 if caller_namespace != target_namespace {
348 return Err(Error::NamespaceTargetMismatch {
349 caller: self.to_owned(),
350 target: target.to_owned(),
351 });
352 }
353
354 if let Some(role) = role {
356 if role == &UserRole::Metrics || role == &UserRole::Backup {
358 return Err(Error::NamespaceRoleInvalid {
359 caller: self.to_owned(),
360 target: target.to_owned(),
361 role: role.to_owned(),
362 });
363 }
364 }
365 } else {
366 return Err(Error::NamespaceSystemWideTarget {
368 caller: self.to_owned(),
369 target: target.to_owned(),
370 });
371 }
372 }
373 } else if let Some(target) = target {
375 if let Some(role) = role {
377 if (role == &UserRole::Metrics || role == &UserRole::Backup)
379 && target.is_namespaced()
380 {
381 return Err(Error::NamespaceRoleInvalid {
382 caller: self.to_owned(),
383 target: target.to_owned(),
384 role: role.to_owned(),
385 });
386 }
387 }
388 }
389 Ok(())
390 }
391}
392
393impl FromStr for UserId {
394 type Err = Error;
395 fn from_str(s: &str) -> Result<Self, Self::Err> {
396 Self::new(s.to_string())
397 }
398}
399
400impl Display for UserId {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 match self {
403 UserId::SystemWide(user_id) => write!(f, "{user_id}"),
404 UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
405 }
406 }
407}
408
409impl From<UserId> for String {
410 fn from(value: UserId) -> Self {
411 value.to_string()
412 }
413}
414
415impl TryFrom<&str> for UserId {
416 type Error = Error;
417
418 fn try_from(value: &str) -> Result<Self, Self::Error> {
419 Self::new(value.to_string())
420 }
421}
422
423impl TryFrom<&String> for UserId {
424 type Error = Error;
425
426 fn try_from(value: &String) -> Result<Self, Self::Error> {
427 Self::new(value.to_string())
428 }
429}
430
431impl TryFrom<String> for UserId {
432 type Error = Error;
433
434 fn try_from(value: String) -> Result<Self, Self::Error> {
435 Self::new(value)
436 }
437}
438
439#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
441#[serde(into = "String", try_from = "String")]
442pub struct SystemWideUserId(UserId);
443
444impl SystemWideUserId {
445 pub fn new(user_id: String) -> Result<Self, Error> {
466 let user_id = UserId::new(user_id)?;
467
468 if user_id.is_namespaced() {
469 return Err(Error::SystemWideUserIdWithNamespace(user_id));
470 }
471
472 Ok(Self(user_id))
473 }
474}
475
476impl AsRef<UserId> for SystemWideUserId {
477 fn as_ref(&self) -> &UserId {
478 &self.0
479 }
480}
481
482impl Display for SystemWideUserId {
483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484 self.0.fmt(f)
485 }
486}
487
488impl FromStr for SystemWideUserId {
489 type Err = Error;
490 fn from_str(s: &str) -> Result<Self, Self::Err> {
491 Self::new(s.to_string())
492 }
493}
494
495impl From<SystemWideUserId> for String {
496 fn from(value: SystemWideUserId) -> Self {
497 value.to_string()
498 }
499}
500
501impl From<SystemWideUserId> for UserId {
502 fn from(value: SystemWideUserId) -> Self {
503 value.0
504 }
505}
506
507impl TryFrom<String> for SystemWideUserId {
508 type Error = Error;
509
510 fn try_from(value: String) -> Result<Self, Self::Error> {
511 Self::new(value)
512 }
513}
514
515#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
517#[serde(into = "String", try_from = "String")]
518pub struct NamespacedUserId(UserId);
519
520impl NamespacedUserId {
521 pub fn new(user_id: String) -> Result<Self, Error> {
542 let user_id = UserId::new(user_id)?;
543
544 if !user_id.is_namespaced() {
545 return Err(Error::NamespacedUserIdWithoutNamespace(user_id));
546 }
547
548 Ok(Self(user_id))
549 }
550}
551
552impl AsRef<UserId> for NamespacedUserId {
553 fn as_ref(&self) -> &UserId {
554 &self.0
555 }
556}
557
558impl Display for NamespacedUserId {
559 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560 self.0.fmt(f)
561 }
562}
563
564impl FromStr for NamespacedUserId {
565 type Err = Error;
566 fn from_str(s: &str) -> Result<Self, Self::Err> {
567 Self::new(s.to_string())
568 }
569}
570
571impl From<NamespacedUserId> for String {
572 fn from(value: NamespacedUserId) -> Self {
573 value.to_string()
574 }
575}
576
577impl From<NamespacedUserId> for UserId {
578 fn from(value: NamespacedUserId) -> Self {
579 value.0
580 }
581}
582
583impl TryFrom<String> for NamespacedUserId {
584 type Error = Error;
585
586 fn try_from(value: String) -> Result<Self, Self::Error> {
587 Self::new(value)
588 }
589}
590
591#[derive(Clone, Debug, Deserialize, Serialize)]
596pub struct FullCredentials {
597 pub name: UserId,
599
600 pub passphrase: Passphrase,
602}
603
604impl FullCredentials {
605 pub fn new(name: UserId, passphrase: Passphrase) -> Self {
619 Self { name, passphrase }
620 }
621}
622
623impl UserWithPassphrase for FullCredentials {
624 fn user(&self) -> String {
625 self.name.to_string()
626 }
627
628 fn passphrase(&self) -> &Passphrase {
629 &self.passphrase
630 }
631}
632
633impl From<FullCredentials> for BasicAuth {
634 fn from(value: FullCredentials) -> Self {
635 Self::from(&value)
636 }
637}
638
639impl From<&FullCredentials> for BasicAuth {
640 fn from(value: &FullCredentials) -> Self {
641 (
642 value.name.to_string(),
643 Some(value.passphrase.expose_owned()),
644 )
645 }
646}
647
648impl TryFrom<&Credentials> for FullCredentials {
649 type Error = Error;
650
651 fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
652 let creds = value.clone();
653 FullCredentials::try_from(creds)
654 }
655}
656
657impl TryFrom<Credentials> for FullCredentials {
658 type Error = Error;
659
660 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
661 let Some(passphrase) = value.passphrase else {
662 return Err(Error::PassphraseMissing {
663 user: value.user_id,
664 });
665 };
666
667 Ok(FullCredentials {
668 name: value.user_id,
669 passphrase,
670 })
671 }
672}
673
674#[derive(Clone, Debug)]
678pub struct Credentials {
679 pub user_id: UserId,
681
682 pub passphrase: Option<Passphrase>,
684}
685
686impl Credentials {
687 pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
703 Self {
704 user_id,
705 passphrase,
706 }
707 }
708}
709
710impl Display for Credentials {
711 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
712 write!(f, "{}", self.user_id)?;
713 if let Some(passphrase) = self.passphrase.as_ref() {
714 write!(f, " ({passphrase})")?;
715 }
716 Ok(())
717 }
718}
719
720impl From<Credentials> for BasicAuth {
721 fn from(value: Credentials) -> Self {
722 (
723 value.user_id.to_string(),
724 value.passphrase.map(|x| x.expose_owned()),
725 )
726 }
727}
728
729impl From<&Credentials> for BasicAuth {
730 fn from(value: &Credentials) -> Self {
731 (
732 value.user_id.to_string(),
733 value.passphrase.as_ref().map(|x| x.expose_owned()),
734 )
735 }
736}
737
738impl From<&FullCredentials> for Credentials {
739 fn from(value: &FullCredentials) -> Self {
740 let creds = value.clone();
741 Self::from(creds)
742 }
743}
744
745impl From<FullCredentials> for Credentials {
746 fn from(value: FullCredentials) -> Self {
747 Credentials::new(value.name, Some(value.passphrase))
748 }
749}
750
751impl TryFrom<Box<dyn UserWithPassphrase>> for Credentials {
752 type Error = crate::Error;
753
754 fn try_from(value: Box<dyn UserWithPassphrase>) -> Result<Self, Self::Error> {
755 Ok(Self::new(
756 UserId::try_from(value.user())?,
757 Some(value.passphrase().clone()),
758 ))
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use rstest::rstest;
765 use testresult::TestResult;
766
767 use super::*;
768
769 #[rstest]
770 #[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
771 #[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
772 fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
773 assert_eq!(credentials.to_string(), expected);
774 Ok(())
775 }
776
777 #[rstest]
778 #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
779 #[case("f", Some(UserId::SystemWide("f".to_string())))]
780 #[case("1", Some(UserId::SystemWide("1".to_string())))]
781 #[case("foo;-", None)]
782 #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
783 #[case("FOO", None)]
784 #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
785 #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
786 #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
787 #[case("~bar", None)]
788 #[case("", None)]
789 #[case("foo;-~bar\\", None)]
790 #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
791 #[case("foo~bar~baz", None)]
792 #[case("FOO~bar", None)]
793 #[case("foo~BAR", None)]
794 fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
795 if let Some(user_id) = user_id {
796 assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
797 } else {
798 assert!(UserId::from_str(input).is_err());
799 }
800
801 Ok(())
802 }
803
804 #[rstest]
805 #[case(UserId::SystemWide("user".to_string()), None)]
806 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
807 fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
808 assert_eq!(input.namespace(), result.as_ref());
809 Ok(())
810 }
811
812 #[rstest]
813 #[case(UserId::SystemWide("user".to_string()), false)]
814 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
815 fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
816 assert_eq!(input.is_namespaced(), result);
817 Ok(())
818 }
819
820 #[rstest]
821 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
822 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
823 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
824 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
825 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
826 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
827 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
828 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
829 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
830 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
831 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
832 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
833 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
834 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
835 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
836 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
837 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
838 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
839 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
840 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
841 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
842 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
843 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
844 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
845 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
846 fn validate_namespace_access(
847 #[case] caller: UserId,
848 #[case] namespace_support: NamespaceSupport,
849 #[case] target: Option<UserId>,
850 #[case] role: Option<UserRole>,
851 #[case] result: Option<()>,
852 ) -> TestResult {
853 if result.is_some() {
854 assert!(
855 caller
856 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
857 .is_ok()
858 );
859 } else {
860 assert!(
861 caller
862 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
863 .is_err()
864 )
865 }
866 Ok(())
867 }
868
869 #[test]
871 fn system_wide_user_id_new_fails_on_user_id_with_namespace() -> TestResult {
872 match SystemWideUserId::new("ns1~test".to_string()) {
873 Err(Error::SystemWideUserIdWithNamespace(_)) => Ok(()),
874 Err(error) => panic!(
875 "Expected to fail with a Error::SystemWideUserIdWithNamespace but got a different error instead:\n{error}"
876 ),
877 Ok(user_id) => panic!(
878 "Expected to fail with a Error::SystemWideUserIdWithNamespace but succeeded instead:\n{user_id}"
879 ),
880 }
881 }
882
883 #[test]
885 fn system_wide_user_id_new_fails_on_invalid_user_id() -> TestResult {
886 match SystemWideUserId::new("test[]".to_string()) {
887 Err(Error::InvalidUserIds { .. }) => Ok(()),
888 Err(error) => panic!(
889 "Expected to fail with a Error::InvalidUserIds but got a different error instead:\n{error}"
890 ),
891 Ok(user_id) => panic!(
892 "Expected to fail with a Error::InvalidUserIds but succeeded instead:\n{user_id}"
893 ),
894 }
895 }
896
897 #[test]
899 fn system_wide_user_id_from_str_succeeds() -> TestResult {
900 assert_eq!(SystemWideUserId::from_str("test")?.to_string(), "test");
901 Ok(())
902 }
903
904 #[test]
906 fn namespaced_user_id_new_fails_on_user_id_without_namespace() -> TestResult {
907 match NamespacedUserId::new("test".to_string()) {
908 Err(Error::NamespacedUserIdWithoutNamespace(_)) => Ok(()),
909 Err(error) => panic!(
910 "Expected to fail with a Error::NamespacedUserIdWithoutNamespace but got a different error instead:\n{error}"
911 ),
912 Ok(user_id) => panic!(
913 "Expected to fail with a Error::NamespacedUserIdWithoutNamespace but succeeded instead:\n{user_id}"
914 ),
915 }
916 }
917
918 #[test]
920 fn namespaced_user_id_new_fails_on_invalid_user_id() -> TestResult {
921 match NamespacedUserId::new("test[]".to_string()) {
922 Err(Error::InvalidUserIds { .. }) => Ok(()),
923 Err(error) => panic!(
924 "Expected to fail with a Error::InvalidUserIds but got a different error instead:\n{error}"
925 ),
926 Ok(user_id) => panic!(
927 "Expected to fail with a Error::InvalidUserIds but succeeded instead:\n{user_id}"
928 ),
929 }
930 }
931
932 #[test]
934 fn namespaced_user_id_from_str_succeeds() -> TestResult {
935 assert_eq!(
936 NamespacedUserId::from_str("ns1~test")?.to_string(),
937 "ns1~test"
938 );
939 Ok(())
940 }
941}