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
77#[derive(AsRefStr, Clone, Debug, strum::Display, Eq, PartialEq)]
81#[strum(serialize_all = "lowercase")]
82pub enum NamespaceSupport {
83 Supported,
85 Unsupported,
87}
88
89#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
97pub struct NamespaceId(String);
98
99impl NamespaceId {
100 pub fn new(namespace_id: String) -> Result<Self, Error> {
124 if namespace_id.is_empty()
125 || !namespace_id.chars().all(|char| {
126 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
127 })
128 {
129 return Err(Error::InvalidNamespaceIds {
130 namespace_ids: vec![namespace_id],
131 });
132 }
133 Ok(Self(namespace_id))
134 }
135}
136
137impl AsRef<str> for NamespaceId {
138 fn as_ref(&self) -> &str {
139 self.0.as_str()
140 }
141}
142
143impl FromStr for NamespaceId {
144 type Err = Error;
145 fn from_str(s: &str) -> Result<Self, Self::Err> {
146 Self::new(s.to_string())
147 }
148}
149
150impl Display for NamespaceId {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "{}", self.0)
153 }
154}
155
156impl TryFrom<&str> for NamespaceId {
157 type Error = Error;
158
159 fn try_from(value: &str) -> Result<Self, Self::Error> {
160 Self::new(value.to_string())
161 }
162}
163
164#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
179#[serde(into = "String", try_from = "String")]
180pub enum UserId {
181 SystemWide(String),
183 Namespace(NamespaceId, String),
185}
186
187impl UserId {
188 pub fn new(user_id: String) -> Result<Self, Error> {
222 if let Some((namespace, name)) = user_id.split_once("~") {
223 if namespace.is_empty()
224 || !(namespace.chars().all(|char| {
225 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
226 }) && name.chars().all(|char| {
227 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
228 }))
229 {
230 return Err(Error::InvalidUserIds {
231 user_ids: vec![user_id],
232 });
233 }
234 Ok(Self::Namespace(namespace.parse()?, name.to_string()))
235 } else {
236 if user_id.is_empty()
237 || !user_id.chars().all(|char| {
238 char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
239 })
240 {
241 return Err(Error::InvalidUserIds {
242 user_ids: vec![user_id],
243 });
244 }
245 Ok(Self::SystemWide(user_id))
246 }
247 }
248
249 pub fn namespace(&self) -> Option<&NamespaceId> {
268 match self {
269 Self::SystemWide(_) => None,
270 Self::Namespace(namespace, _) => Some(namespace),
271 }
272 }
273
274 pub fn is_namespaced(&self) -> bool {
293 match self {
294 Self::SystemWide(_) => false,
295 Self::Namespace(_, _) => true,
296 }
297 }
298
299 pub fn validate_namespace_access(
322 &self,
323 support: NamespaceSupport,
324 target: Option<&UserId>,
325 role: Option<&UserRole>,
326 ) -> Result<(), Error> {
327 if let Some(caller_namespace) = self.namespace() {
329 if support == NamespaceSupport::Unsupported {
331 return Err(Error::NamespaceUnsupported(self.to_owned()));
332 }
333
334 if let Some(target) = target {
336 if let Some(target_namespace) = target.namespace() {
338 if caller_namespace != target_namespace {
340 return Err(Error::NamespaceTargetMismatch {
341 caller: self.to_owned(),
342 target: target.to_owned(),
343 });
344 }
345
346 if let Some(role) = role {
348 if role == &UserRole::Metrics || role == &UserRole::Backup {
350 return Err(Error::NamespaceRoleInvalid {
351 caller: self.to_owned(),
352 target: target.to_owned(),
353 role: role.to_owned(),
354 });
355 }
356 }
357 } else {
358 return Err(Error::NamespaceSystemWideTarget {
360 caller: self.to_owned(),
361 target: target.to_owned(),
362 });
363 }
364 }
365 } else if let Some(target) = target {
367 if let Some(role) = role {
369 if (role == &UserRole::Metrics || role == &UserRole::Backup)
371 && target.is_namespaced()
372 {
373 return Err(Error::NamespaceRoleInvalid {
374 caller: self.to_owned(),
375 target: target.to_owned(),
376 role: role.to_owned(),
377 });
378 }
379 }
380 }
381 Ok(())
382 }
383}
384
385impl FromStr for UserId {
386 type Err = Error;
387 fn from_str(s: &str) -> Result<Self, Self::Err> {
388 Self::new(s.to_string())
389 }
390}
391
392impl Display for UserId {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 match self {
395 UserId::SystemWide(user_id) => write!(f, "{user_id}"),
396 UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
397 }
398 }
399}
400
401impl From<UserId> for String {
402 fn from(value: UserId) -> Self {
403 value.to_string()
404 }
405}
406
407impl TryFrom<&str> for UserId {
408 type Error = Error;
409
410 fn try_from(value: &str) -> Result<Self, Self::Error> {
411 Self::new(value.to_string())
412 }
413}
414
415impl TryFrom<&String> for UserId {
416 type Error = Error;
417
418 fn try_from(value: &String) -> 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)
428 }
429}
430
431#[derive(Clone, Debug, Deserialize, Serialize)]
436pub struct FullCredentials {
437 pub name: UserId,
439
440 pub passphrase: Passphrase,
442}
443
444impl FullCredentials {
445 pub fn new(name: UserId, passphrase: Passphrase) -> Self {
459 Self { name, passphrase }
460 }
461}
462
463impl UserWithPassphrase for FullCredentials {
464 fn user(&self) -> String {
465 self.name.to_string()
466 }
467
468 fn passphrase(&self) -> &Passphrase {
469 &self.passphrase
470 }
471}
472
473impl From<FullCredentials> for BasicAuth {
474 fn from(value: FullCredentials) -> Self {
475 Self::from(&value)
476 }
477}
478
479impl From<&FullCredentials> for BasicAuth {
480 fn from(value: &FullCredentials) -> Self {
481 (
482 value.name.to_string(),
483 Some(value.passphrase.expose_owned()),
484 )
485 }
486}
487
488impl TryFrom<&Credentials> for FullCredentials {
489 type Error = Error;
490
491 fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
492 let creds = value.clone();
493 FullCredentials::try_from(creds)
494 }
495}
496
497impl TryFrom<Credentials> for FullCredentials {
498 type Error = Error;
499
500 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
501 let Some(passphrase) = value.passphrase else {
502 return Err(Error::PassphraseMissing {
503 user: value.user_id,
504 });
505 };
506
507 Ok(FullCredentials {
508 name: value.user_id,
509 passphrase,
510 })
511 }
512}
513
514#[derive(Clone, Debug)]
518pub struct Credentials {
519 pub user_id: UserId,
521
522 pub passphrase: Option<Passphrase>,
524}
525
526impl Credentials {
527 pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
543 Self {
544 user_id,
545 passphrase,
546 }
547 }
548}
549
550impl Display for Credentials {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 write!(f, "{}", self.user_id)?;
553 if let Some(passphrase) = self.passphrase.as_ref() {
554 write!(f, " ({passphrase})")?;
555 }
556 Ok(())
557 }
558}
559
560impl From<Credentials> for BasicAuth {
561 fn from(value: Credentials) -> Self {
562 (
563 value.user_id.to_string(),
564 value.passphrase.map(|x| x.expose_owned()),
565 )
566 }
567}
568
569impl From<&Credentials> for BasicAuth {
570 fn from(value: &Credentials) -> Self {
571 (
572 value.user_id.to_string(),
573 value.passphrase.as_ref().map(|x| x.expose_owned()),
574 )
575 }
576}
577
578impl From<&FullCredentials> for Credentials {
579 fn from(value: &FullCredentials) -> Self {
580 let creds = value.clone();
581 Self::from(creds)
582 }
583}
584
585impl From<FullCredentials> for Credentials {
586 fn from(value: FullCredentials) -> Self {
587 Credentials::new(value.name, Some(value.passphrase))
588 }
589}
590
591impl TryFrom<Box<dyn UserWithPassphrase>> for Credentials {
592 type Error = crate::Error;
593
594 fn try_from(value: Box<dyn UserWithPassphrase>) -> Result<Self, Self::Error> {
595 Ok(Self::new(
596 UserId::try_from(value.user())?,
597 Some(value.passphrase().clone()),
598 ))
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use rstest::rstest;
605 use testresult::TestResult;
606
607 use super::*;
608
609 #[rstest]
610 #[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
611 #[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
612 fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
613 assert_eq!(credentials.to_string(), expected);
614 Ok(())
615 }
616
617 #[rstest]
618 #[case("foo", Some(UserId::SystemWide("foo".to_string())))]
619 #[case("f", Some(UserId::SystemWide("f".to_string())))]
620 #[case("1", Some(UserId::SystemWide("1".to_string())))]
621 #[case("foo;-", None)]
622 #[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
623 #[case("FOO", None)]
624 #[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
625 #[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
626 #[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
627 #[case("~bar", None)]
628 #[case("", None)]
629 #[case("foo;-~bar\\", None)]
630 #[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
631 #[case("foo~bar~baz", None)]
632 #[case("FOO~bar", None)]
633 #[case("foo~BAR", None)]
634 fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
635 if let Some(user_id) = user_id {
636 assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
637 } else {
638 assert!(UserId::from_str(input).is_err());
639 }
640
641 Ok(())
642 }
643
644 #[rstest]
645 #[case(UserId::SystemWide("user".to_string()), None)]
646 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
647 fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
648 assert_eq!(input.namespace(), result.as_ref());
649 Ok(())
650 }
651
652 #[rstest]
653 #[case(UserId::SystemWide("user".to_string()), false)]
654 #[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
655 fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
656 assert_eq!(input.is_namespaced(), result);
657 Ok(())
658 }
659
660 #[rstest]
661 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
662 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
663 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
664 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
665 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
666 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
667 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
668 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
669 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
670 #[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
671 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
672 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
673 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
674 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
675 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
676 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
677 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
678 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
679 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
680 #[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
681 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
682 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
683 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
684 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
685 #[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
686 fn validate_namespace_access(
687 #[case] caller: UserId,
688 #[case] namespace_support: NamespaceSupport,
689 #[case] target: Option<UserId>,
690 #[case] role: Option<UserRole>,
691 #[case] result: Option<()>,
692 ) -> TestResult {
693 if result.is_some() {
694 assert!(
695 caller
696 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
697 .is_ok()
698 );
699 } else {
700 assert!(
701 caller
702 .validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
703 .is_err()
704 )
705 }
706 Ok(())
707 }
708}