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 IDs: {}", namespace_ids.join(", "))]
28 InvalidNamespaceIds {
29 namespace_ids: Vec<String>,
31 },
32
33 #[error("Invalid User IDs: {}", user_ids.join(", "))]
35 InvalidUserIds {
36 user_ids: Vec<String>,
38 },
39
40 #[error("The calling user {0} is in a namespace, which is not supported in this context.")]
42 NamespaceUnsupported(UserId),
43
44 #[error("User {caller} targets {target} which is in a different namespace")]
46 NamespaceTargetMismatch {
47 caller: UserId,
49
50 target: UserId,
52 },
53
54 #[error("User {caller} targets {target} a system-wide user")]
56 NamespaceSystemWideTarget {
57 caller: UserId,
59
60 target: UserId,
62 },
63
64 #[error(
66 "User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
67 )]
68 NamespaceRoleInvalid {
69 caller: UserId,
71
72 target: UserId,
74
75 role: UserRole,
77 },
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum NamespaceSupport {
85 Supported,
87 Unsupported,
89}
90
91#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
99pub struct NamespaceId(String);
100
101impl NamespaceId {
102 pub fn new(namespace_id: String) -> Result<Self, Error> {
126 if namespace_id.is_empty()
127 || !namespace_id.chars().all(|char| {
128 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
129 })
130 {
131 return Err(Error::InvalidNamespaceIds {
132 namespace_ids: vec![namespace_id],
133 });
134 }
135 Ok(Self(namespace_id))
136 }
137}
138
139impl AsRef<str> for NamespaceId {
140 fn as_ref(&self) -> &str {
141 self.0.as_str()
142 }
143}
144
145impl FromStr for NamespaceId {
146 type Err = Error;
147 fn from_str(s: &str) -> Result<Self, Self::Err> {
148 Self::new(s.to_string())
149 }
150}
151
152impl Display for NamespaceId {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "{}", self.0)
155 }
156}
157
158impl TryFrom<&str> for NamespaceId {
159 type Error = Error;
160
161 fn try_from(value: &str) -> Result<Self, Self::Error> {
162 Self::new(value.to_string())
163 }
164}
165
166#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
181#[serde(into = "String", try_from = "String")]
182pub enum UserId {
183 SystemWide(String),
185 Namespace(NamespaceId, String),
187}
188
189impl UserId {
190 pub fn new(user_id: String) -> Result<Self, Error> {
224 if let Some((namespace, name)) = user_id.split_once("~") {
225 if namespace.is_empty()
226 || !(namespace.chars().all(|char| {
227 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
228 }) && name.chars().all(|char| {
229 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
230 }))
231 {
232 return Err(Error::InvalidUserIds {
233 user_ids: vec![user_id],
234 });
235 }
236 Ok(Self::Namespace(namespace.parse()?, name.to_string()))
237 } else {
238 if user_id.is_empty()
239 || !user_id.chars().all(|char| {
240 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
241 })
242 {
243 return Err(Error::InvalidUserIds {
244 user_ids: vec![user_id],
245 });
246 }
247 Ok(Self::SystemWide(user_id))
248 }
249 }
250
251 pub fn namespace(&self) -> Option<&NamespaceId> {
270 match self {
271 Self::SystemWide(_) => None,
272 Self::Namespace(namespace, _) => Some(namespace),
273 }
274 }
275
276 pub fn is_namespaced(&self) -> bool {
295 match self {
296 Self::SystemWide(_) => false,
297 Self::Namespace(_, _) => true,
298 }
299 }
300
301 pub fn validate_namespace_access(
324 &self,
325 support: NamespaceSupport,
326 target: Option<&UserId>,
327 role: Option<&UserRole>,
328 ) -> Result<(), Error> {
329 if let Some(caller_namespace) = self.namespace() {
331 if support == NamespaceSupport::Unsupported {
333 return Err(Error::NamespaceUnsupported(self.to_owned()));
334 }
335
336 if let Some(target) = target {
338 if let Some(target_namespace) = target.namespace() {
340 if caller_namespace != target_namespace {
342 return Err(Error::NamespaceTargetMismatch {
343 caller: self.to_owned(),
344 target: target.to_owned(),
345 });
346 }
347
348 if let Some(role) = role {
350 if role == &UserRole::Metrics || role == &UserRole::Backup {
352 return Err(Error::NamespaceRoleInvalid {
353 caller: self.to_owned(),
354 target: target.to_owned(),
355 role: role.to_owned(),
356 });
357 }
358 }
359 } else {
360 return Err(Error::NamespaceSystemWideTarget {
362 caller: self.to_owned(),
363 target: target.to_owned(),
364 });
365 }
366 }
367 } else if let Some(target) = target {
369 if let Some(role) = role {
371 if (role == &UserRole::Metrics || role == &UserRole::Backup)
373 && target.is_namespaced()
374 {
375 return Err(Error::NamespaceRoleInvalid {
376 caller: self.to_owned(),
377 target: target.to_owned(),
378 role: role.to_owned(),
379 });
380 }
381 }
382 }
383 Ok(())
384 }
385}
386
387impl FromStr for UserId {
388 type Err = Error;
389 fn from_str(s: &str) -> Result<Self, Self::Err> {
390 Self::new(s.to_string())
391 }
392}
393
394impl Display for UserId {
395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396 match self {
397 UserId::SystemWide(user_id) => write!(f, "{user_id}"),
398 UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
399 }
400 }
401}
402
403impl From<UserId> for String {
404 fn from(value: UserId) -> Self {
405 value.to_string()
406 }
407}
408
409impl TryFrom<&str> for UserId {
410 type Error = Error;
411
412 fn try_from(value: &str) -> Result<Self, Self::Error> {
413 Self::new(value.to_string())
414 }
415}
416
417impl TryFrom<&String> for UserId {
418 type Error = Error;
419
420 fn try_from(value: &String) -> Result<Self, Self::Error> {
421 Self::new(value.to_string())
422 }
423}
424
425impl TryFrom<String> for UserId {
426 type Error = Error;
427
428 fn try_from(value: String) -> Result<Self, Self::Error> {
429 Self::new(value)
430 }
431}
432
433#[derive(Clone, Debug, Deserialize, Serialize)]
438pub struct FullCredentials {
439 pub name: UserId,
441
442 pub passphrase: Passphrase,
444}
445
446impl FullCredentials {
447 pub fn new(name: UserId, passphrase: Passphrase) -> Self {
461 Self { name, passphrase }
462 }
463}
464
465impl From<FullCredentials> for BasicAuth {
466 fn from(value: FullCredentials) -> Self {
467 Self::from(&value)
468 }
469}
470
471impl From<&FullCredentials> for BasicAuth {
472 fn from(value: &FullCredentials) -> Self {
473 (
474 value.name.to_string(),
475 Some(value.passphrase.expose_owned()),
476 )
477 }
478}
479
480impl TryFrom<&Credentials> for FullCredentials {
481 type Error = Error;
482
483 fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
484 let creds = value.clone();
485 FullCredentials::try_from(creds)
486 }
487}
488
489impl TryFrom<Credentials> for FullCredentials {
490 type Error = Error;
491
492 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
493 let Some(passphrase) = value.passphrase else {
494 return Err(Error::PassphraseMissing {
495 user: value.user_id,
496 });
497 };
498
499 Ok(FullCredentials {
500 name: value.user_id,
501 passphrase,
502 })
503 }
504}
505
506#[derive(Clone, Debug)]
510pub struct Credentials {
511 pub user_id: UserId,
513
514 pub passphrase: Option<Passphrase>,
516}
517
518impl Credentials {
519 pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
535 Self {
536 user_id,
537 passphrase,
538 }
539 }
540}
541
542impl From<Credentials> for BasicAuth {
543 fn from(value: Credentials) -> Self {
544 (
545 value.user_id.to_string(),
546 value.passphrase.map(|x| x.expose_owned()),
547 )
548 }
549}
550
551impl From<&Credentials> for BasicAuth {
552 fn from(value: &Credentials) -> Self {
553 (
554 value.user_id.to_string(),
555 value.passphrase.as_ref().map(|x| x.expose_owned()),
556 )
557 }
558}
559
560impl From<&FullCredentials> for Credentials {
561 fn from(value: &FullCredentials) -> Self {
562 let creds = value.clone();
563 Self::from(creds)
564 }
565}
566
567impl From<FullCredentials> for Credentials {
568 fn from(value: FullCredentials) -> Self {
569 Credentials::new(value.name, Some(value.passphrase))
570 }
571}
572
573#[derive(Clone, Debug, Default, Deserialize)]
578pub struct Passphrase(SecretString);
579
580impl Passphrase {
581 pub fn new(passphrase: String) -> Self {
590 Self(SecretString::new(passphrase.into()))
591 }
592
593 pub fn expose_owned(&self) -> String {
598 self.0.expose_secret().to_owned()
599 }
600
601 pub fn expose_borrowed(&self) -> &str {
603 self.0.expose_secret()
604 }
605}
606
607impl FromStr for Passphrase {
608 type Err = Error;
609
610 fn from_str(s: &str) -> Result<Self, Self::Err> {
611 Ok(Self(SecretString::from(s.to_string())))
612 }
613}
614
615impl Serialize for Passphrase {
616 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
623 where
624 S: serde::Serializer,
625 {
626 self.0.expose_secret().serialize(serializer)
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use rstest::rstest;
633 use testresult::TestResult;
634
635 use super::*;
636
637 #[rstest]
638 #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
639 #[case("f", Some(UserId::SystemWide("f".to_string())))]
640 #[case("1", Some(UserId::SystemWide("1".to_string())))]
641 #[case("foo;-", None)]
642 #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
643 #[case("FOO", None)]
644 #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
645 #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
646 #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
647 #[case("~bar", None)]
648 #[case("", None)]
649 #[case("foo;-~bar\\", None)]
650 #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
651 #[case("foo~bar~baz", None)]
652 #[case("FOO~bar", None)]
653 #[case("foo~BAR", None)]
654 fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
655 if let Some(user_id) = user_id {
656 assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
657 } else {
658 assert!(UserId::from_str(input).is_err());
659 }
660
661 Ok(())
662 }
663
664 #[rstest]
665 #[case(UserId::SystemWide("user".to_string()), None)]
666 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
667 fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
668 assert_eq!(input.namespace(), result.as_ref());
669 Ok(())
670 }
671
672 #[rstest]
673 #[case(UserId::SystemWide("user".to_string()), false)]
674 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
675 fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
676 assert_eq!(input.is_namespaced(), result);
677 Ok(())
678 }
679
680 #[rstest]
681 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
682 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
683 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
684 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
685 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
686 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
687 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
688 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
689 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
690 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
691 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
692 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
693 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
694 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
695 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
696 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
697 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
698 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
699 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
700 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
701 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
702 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
703 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
704 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
705 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
706 fn validate_namespace_access(
707 #[case] caller: UserId,
708 #[case] namespace_support: NamespaceSupport,
709 #[case] target: Option<UserId>,
710 #[case] role: Option<UserRole>,
711 #[case] result: Option<()>,
712 ) -> TestResult {
713 if result.is_some() {
714 assert!(
715 caller
716 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
717 .is_ok()
718 );
719 } else {
720 assert!(
721 caller
722 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
723 .is_err()
724 )
725 }
726 Ok(())
727 }
728}