1use std::fmt::Display;
4use std::str::FromStr;
5
6use nethsm_sdk_rs::apis::configuration::BasicAuth;
7use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize};
9use strum::AsRefStr;
10
11use crate::UserRole;
12
13#[derive(Debug, thiserror::Error)]
15pub enum Error {
16 #[error("Unable to convert string to passphrase")]
18 Passphrase,
19
20 #[error("The passphrase for user {user} is missing")]
22 PassphraseMissing {
23 user: UserId,
25 },
26
27 #[error("Invalid Namespace IDs: {}", namespace_ids.join(", "))]
29 InvalidNamespaceIds {
30 namespace_ids: Vec<String>,
32 },
33
34 #[error("Invalid User IDs: {}", user_ids.join(", "))]
36 InvalidUserIds {
37 user_ids: Vec<String>,
39 },
40
41 #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
43 NamespaceUnsupported(UserId),
44
45 #[error("User {caller} targets {target} which is in a different namespace")]
47 NamespaceTargetMismatch {
48 caller: UserId,
50
51 target: UserId,
53 },
54
55 #[error("User {caller} targets {target} a system-wide user")]
57 NamespaceSystemWideTarget {
58 caller: UserId,
60
61 target: UserId,
63 },
64
65 #[error(
67 "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
68 )]
69 NamespaceRoleInvalid {
70 caller: UserId,
72
73 target: UserId,
75
76 role: UserRole,
78 },
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, Serialize)]
440pub struct FullCredentials {
441 pub name: UserId,
443
444 pub passphrase: Passphrase,
446}
447
448impl FullCredentials {
449 pub fn new(name: UserId, passphrase: Passphrase) -> Self {
463 Self { name, passphrase }
464 }
465}
466
467impl From<FullCredentials> for BasicAuth {
468 fn from(value: FullCredentials) -> Self {
469 Self::from(&value)
470 }
471}
472
473impl From<&FullCredentials> for BasicAuth {
474 fn from(value: &FullCredentials) -> Self {
475 (
476 value.name.to_string(),
477 Some(value.passphrase.expose_owned()),
478 )
479 }
480}
481
482impl TryFrom<&Credentials> for FullCredentials {
483 type Error = Error;
484
485 fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
486 let creds = value.clone();
487 FullCredentials::try_from(creds)
488 }
489}
490
491impl TryFrom<Credentials> for FullCredentials {
492 type Error = Error;
493
494 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
495 let Some(passphrase) = value.passphrase else {
496 return Err(Error::PassphraseMissing {
497 user: value.user_id,
498 });
499 };
500
501 Ok(FullCredentials {
502 name: value.user_id,
503 passphrase,
504 })
505 }
506}
507
508#[derive(Clone, Debug)]
512pub struct Credentials {
513 pub user_id: UserId,
515
516 pub passphrase: Option<Passphrase>,
518}
519
520impl Credentials {
521 pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
537 Self {
538 user_id,
539 passphrase,
540 }
541 }
542}
543
544impl Display for Credentials {
545 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546 write!(f, "{}", self.user_id)?;
547 if let Some(passphrase) = self.passphrase.as_ref() {
548 write!(f, " ({passphrase})")?;
549 }
550 Ok(())
551 }
552}
553
554impl From<Credentials> for BasicAuth {
555 fn from(value: Credentials) -> Self {
556 (
557 value.user_id.to_string(),
558 value.passphrase.map(|x| x.expose_owned()),
559 )
560 }
561}
562
563impl From<&Credentials> for BasicAuth {
564 fn from(value: &Credentials) -> Self {
565 (
566 value.user_id.to_string(),
567 value.passphrase.as_ref().map(|x| x.expose_owned()),
568 )
569 }
570}
571
572impl From<&FullCredentials> for Credentials {
573 fn from(value: &FullCredentials) -> Self {
574 let creds = value.clone();
575 Self::from(creds)
576 }
577}
578
579impl From<FullCredentials> for Credentials {
580 fn from(value: FullCredentials) -> Self {
581 Credentials::new(value.name, Some(value.passphrase))
582 }
583}
584
585#[derive(Clone, Debug, Default, Deserialize)]
590pub struct Passphrase(SecretString);
591
592impl Passphrase {
593 pub fn new(passphrase: String) -> Self {
602 Self(SecretString::new(passphrase.into()))
603 }
604
605 pub fn expose_owned(&self) -> String {
610 self.0.expose_secret().to_owned()
611 }
612
613 pub fn expose_borrowed(&self) -> &str {
615 self.0.expose_secret()
616 }
617}
618
619impl Display for Passphrase {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 write!(f, "[REDACTED]")
622 }
623}
624
625impl FromStr for Passphrase {
626 type Err = Error;
627
628 fn from_str(s: &str) -> Result<Self, Self::Err> {
629 Ok(Self(SecretString::from(s.to_string())))
630 }
631}
632
633impl Serialize for Passphrase {
634 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
641 where
642 S: serde::Serializer,
643 {
644 self.0.expose_secret().serialize(serializer)
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use rstest::rstest;
651 use testresult::TestResult;
652
653 use super::*;
654
655 #[test]
656 fn passphrase_display() -> TestResult {
657 let passphrase = Passphrase::new("a-secret-passphrase".to_string());
658 assert_eq!(format!("{passphrase}"), "[REDACTED]");
659 Ok(())
660 }
661
662 #[rstest]
663 #[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
664 #[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
665 fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
666 assert_eq!(credentials.to_string(), expected);
667 Ok(())
668 }
669
670 #[rstest]
671 #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
672 #[case("f", Some(UserId::SystemWide("f".to_string())))]
673 #[case("1", Some(UserId::SystemWide("1".to_string())))]
674 #[case("foo;-", None)]
675 #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
676 #[case("FOO", None)]
677 #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
678 #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
679 #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
680 #[case("~bar", None)]
681 #[case("", None)]
682 #[case("foo;-~bar\\", None)]
683 #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
684 #[case("foo~bar~baz", None)]
685 #[case("FOO~bar", None)]
686 #[case("foo~BAR", None)]
687 fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
688 if let Some(user_id) = user_id {
689 assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
690 } else {
691 assert!(UserId::from_str(input).is_err());
692 }
693
694 Ok(())
695 }
696
697 #[rstest]
698 #[case(UserId::SystemWide("user".to_string()), None)]
699 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
700 fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
701 assert_eq!(input.namespace(), result.as_ref());
702 Ok(())
703 }
704
705 #[rstest]
706 #[case(UserId::SystemWide("user".to_string()), false)]
707 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
708 fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
709 assert_eq!(input.is_namespaced(), result);
710 Ok(())
711 }
712
713 #[rstest]
714 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
715 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
716 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
717 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
718 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
719 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
720 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
721 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
722 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
723 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
724 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
725 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
726 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
727 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
728 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
729 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
730 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
731 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
732 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
733 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
734 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
735 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
736 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
737 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
738 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
739 fn validate_namespace_access(
740 #[case] caller: UserId,
741 #[case] namespace_support: NamespaceSupport,
742 #[case] target: Option<UserId>,
743 #[case] role: Option<UserRole>,
744 #[case] result: Option<()>,
745 ) -> TestResult {
746 if result.is_some() {
747 assert!(
748 caller
749 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
750 .is_ok()
751 );
752 } else {
753 assert!(
754 caller
755 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
756 .is_err()
757 )
758 }
759 Ok(())
760 }
761}