use std::fmt::Display;
use std::str::FromStr;
use nethsm_sdk_rs::apis::configuration::BasicAuth;
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use strum::AsRefStr;
use crate::UserRole;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Unable to convert string to passphrase")]
Passphrase,
#[error("The passphrase for user {user} is missing")]
PassphraseMissing {
user: UserId,
},
#[error("Invalid Namespace IDs: {}", namespace_ids.join(", "))]
InvalidNamespaceIds {
namespace_ids: Vec<String>,
},
#[error("Invalid User IDs: {}", user_ids.join(", "))]
InvalidUserIds {
user_ids: Vec<String>,
},
#[error("The calling user {0} is in a namespace, which is not supported in this context.")]
NamespaceUnsupported(UserId),
#[error("User {caller} targets {target} which is in a different namespace")]
NamespaceTargetMismatch {
caller: UserId,
target: UserId,
},
#[error("User {caller} targets {target} a system-wide user")]
NamespaceSystemWideTarget {
caller: UserId,
target: UserId,
},
#[error(
"User {caller} attempts to create user {target} in role {role} which is not supported in namespaces"
)]
NamespaceRoleInvalid {
caller: UserId,
target: UserId,
role: UserRole,
},
}
#[derive(AsRefStr, Clone, Debug, strum::Display, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum NamespaceSupport {
Supported,
Unsupported,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct NamespaceId(String);
impl NamespaceId {
pub fn new(namespace_id: String) -> Result<Self, Error> {
if namespace_id.is_empty()
|| !namespace_id.chars().all(|char| {
char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
})
{
return Err(Error::InvalidNamespaceIds {
namespace_ids: vec![namespace_id],
});
}
Ok(Self(namespace_id))
}
}
impl AsRef<str> for NamespaceId {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl FromStr for NamespaceId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_string())
}
}
impl Display for NamespaceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<&str> for NamespaceId {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value.to_string())
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(into = "String", try_from = "String")]
pub enum UserId {
SystemWide(String),
Namespace(NamespaceId, String),
}
impl UserId {
pub fn new(user_id: String) -> Result<Self, Error> {
if let Some((namespace, name)) = user_id.split_once("~") {
if namespace.is_empty()
|| !(namespace.chars().all(|char| {
char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
}) && name.chars().all(|char| {
char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
}))
{
return Err(Error::InvalidUserIds {
user_ids: vec![user_id],
});
}
Ok(Self::Namespace(namespace.parse()?, name.to_string()))
} else {
if user_id.is_empty()
|| !user_id.chars().all(|char| {
char.is_numeric() || (char.is_ascii_lowercase() && char.is_ascii_alphabetic())
})
{
return Err(Error::InvalidUserIds {
user_ids: vec![user_id],
});
}
Ok(Self::SystemWide(user_id))
}
}
pub fn namespace(&self) -> Option<&NamespaceId> {
match self {
Self::SystemWide(_) => None,
Self::Namespace(namespace, _) => Some(namespace),
}
}
pub fn is_namespaced(&self) -> bool {
match self {
Self::SystemWide(_) => false,
Self::Namespace(_, _) => true,
}
}
pub fn validate_namespace_access(
&self,
support: NamespaceSupport,
target: Option<&UserId>,
role: Option<&UserRole>,
) -> Result<(), Error> {
if let Some(caller_namespace) = self.namespace() {
if support == NamespaceSupport::Unsupported {
return Err(Error::NamespaceUnsupported(self.to_owned()));
}
if let Some(target) = target {
if let Some(target_namespace) = target.namespace() {
if caller_namespace != target_namespace {
return Err(Error::NamespaceTargetMismatch {
caller: self.to_owned(),
target: target.to_owned(),
});
}
if let Some(role) = role {
if role == &UserRole::Metrics || role == &UserRole::Backup {
return Err(Error::NamespaceRoleInvalid {
caller: self.to_owned(),
target: target.to_owned(),
role: role.to_owned(),
});
}
}
} else {
return Err(Error::NamespaceSystemWideTarget {
caller: self.to_owned(),
target: target.to_owned(),
});
}
}
} else if let Some(target) = target {
if let Some(role) = role {
if (role == &UserRole::Metrics || role == &UserRole::Backup)
&& target.is_namespaced()
{
return Err(Error::NamespaceRoleInvalid {
caller: self.to_owned(),
target: target.to_owned(),
role: role.to_owned(),
});
}
}
}
Ok(())
}
}
impl FromStr for UserId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_string())
}
}
impl Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserId::SystemWide(user_id) => write!(f, "{user_id}"),
UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
}
}
}
impl From<UserId> for String {
fn from(value: UserId) -> Self {
value.to_string()
}
}
impl TryFrom<&str> for UserId {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value.to_string())
}
}
impl TryFrom<&String> for UserId {
type Error = Error;
fn try_from(value: &String) -> Result<Self, Self::Error> {
Self::new(value.to_string())
}
}
impl TryFrom<String> for UserId {
type Error = Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FullCredentials {
pub name: UserId,
pub passphrase: Passphrase,
}
impl FullCredentials {
pub fn new(name: UserId, passphrase: Passphrase) -> Self {
Self { name, passphrase }
}
}
impl From<FullCredentials> for BasicAuth {
fn from(value: FullCredentials) -> Self {
Self::from(&value)
}
}
impl From<&FullCredentials> for BasicAuth {
fn from(value: &FullCredentials) -> Self {
(
value.name.to_string(),
Some(value.passphrase.expose_owned()),
)
}
}
impl TryFrom<&Credentials> for FullCredentials {
type Error = Error;
fn try_from(value: &Credentials) -> Result<Self, Self::Error> {
let creds = value.clone();
FullCredentials::try_from(creds)
}
}
impl TryFrom<Credentials> for FullCredentials {
type Error = Error;
fn try_from(value: Credentials) -> Result<Self, Self::Error> {
let Some(passphrase) = value.passphrase else {
return Err(Error::PassphraseMissing {
user: value.user_id,
});
};
Ok(FullCredentials {
name: value.user_id,
passphrase,
})
}
}
#[derive(Clone, Debug)]
pub struct Credentials {
pub user_id: UserId,
pub passphrase: Option<Passphrase>,
}
impl Credentials {
pub fn new(user_id: UserId, passphrase: Option<Passphrase>) -> Self {
Self {
user_id,
passphrase,
}
}
}
impl Display for Credentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.user_id)?;
if let Some(passphrase) = self.passphrase.as_ref() {
write!(f, " ({passphrase})")?;
}
Ok(())
}
}
impl From<Credentials> for BasicAuth {
fn from(value: Credentials) -> Self {
(
value.user_id.to_string(),
value.passphrase.map(|x| x.expose_owned()),
)
}
}
impl From<&Credentials> for BasicAuth {
fn from(value: &Credentials) -> Self {
(
value.user_id.to_string(),
value.passphrase.as_ref().map(|x| x.expose_owned()),
)
}
}
impl From<&FullCredentials> for Credentials {
fn from(value: &FullCredentials) -> Self {
let creds = value.clone();
Self::from(creds)
}
}
impl From<FullCredentials> for Credentials {
fn from(value: FullCredentials) -> Self {
Credentials::new(value.name, Some(value.passphrase))
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Passphrase(SecretString);
impl Passphrase {
pub fn new(passphrase: String) -> Self {
Self(SecretString::new(passphrase.into()))
}
pub fn expose_owned(&self) -> String {
self.0.expose_secret().to_owned()
}
pub fn expose_borrowed(&self) -> &str {
self.0.expose_secret()
}
}
impl Display for Passphrase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[REDACTED]")
}
}
impl FromStr for Passphrase {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(SecretString::from(s.to_string())))
}
}
impl Serialize for Passphrase {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.expose_secret().serialize(serializer)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[test]
fn passphrase_display() -> TestResult {
let passphrase = Passphrase::new("a-secret-passphrase".to_string());
assert_eq!(format!("{passphrase}"), "[REDACTED]");
Ok(())
}
#[rstest]
#[case(Credentials::new(UserId::new("user".to_string())?, Some(Passphrase::new("a-secret-passphrase".to_string()))), "user ([REDACTED])")]
#[case(Credentials::new(UserId::new("user".to_string())?, None), "user")]
fn credentials_display(#[case] credentials: Credentials, #[case] expected: &str) -> TestResult {
assert_eq!(credentials.to_string(), expected);
Ok(())
}
#[rstest]
#[case("foo", Some(UserId::SystemWide("foo".to_string())))]
#[case("f", Some(UserId::SystemWide("f".to_string())))]
#[case("1", Some(UserId::SystemWide("1".to_string())))]
#[case("foo;-", None)]
#[case("foo23", Some(UserId::SystemWide("foo23".to_string())))]
#[case("FOO", None)]
#[case("foo~bar", Some(UserId::Namespace(NamespaceId("foo".to_string()), "bar".to_string())))]
#[case("a~b", Some(UserId::Namespace(NamespaceId("a".to_string()), "b".to_string())))]
#[case("1~bar", Some(UserId::Namespace(NamespaceId("1".to_string()), "bar".to_string())))]
#[case("~bar", None)]
#[case("", None)]
#[case("foo;-~bar\\", None)]
#[case("foo23~bar5", Some(UserId::Namespace(NamespaceId("foo23".to_string()), "bar5".to_string())))]
#[case("foo~bar~baz", None)]
#[case("FOO~bar", None)]
#[case("foo~BAR", None)]
fn create_user_id(#[case] input: &str, #[case] user_id: Option<UserId>) -> TestResult {
if let Some(user_id) = user_id {
assert_eq!(UserId::from_str(input)?.to_string(), user_id.to_string());
} else {
assert!(UserId::from_str(input).is_err());
}
Ok(())
}
#[rstest]
#[case(UserId::SystemWide("user".to_string()), None)]
#[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), Some(NamespaceId("namespace".to_string())))]
fn user_id_namespace(#[case] input: UserId, #[case] result: Option<NamespaceId>) -> TestResult {
assert_eq!(input.namespace(), result.as_ref());
Ok(())
}
#[rstest]
#[case(UserId::SystemWide("user".to_string()), false)]
#[case(UserId::Namespace(NamespaceId("namespace".to_string()), "user".to_string()), true)]
fn user_id_in_namespace(#[case] input: UserId, #[case] result: bool) -> TestResult {
assert_eq!(input.is_namespaced(), result);
Ok(())
}
#[rstest]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
#[case(UserId::from_str("user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns1~user2")?), None, None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("ns2~user1")?), None, None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Unsupported, Some(UserId::from_str("user2")?), None, None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns2~user1")?), None, None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), None, Some(()))]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Administrator), Some(()))]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Operator), Some(()))]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Metrics), None)]
#[case(UserId::from_str("ns1~user")?, NamespaceSupport::Supported, Some(UserId::from_str("ns1~user2")?), Some(UserRole::Backup), None)]
#[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), None, Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Administrator), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Operator), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Metrics), Some(()))]
#[case(UserId::from_str("user")?, NamespaceSupport::Supported, Some(UserId::from_str("user2")?), Some(UserRole::Backup), Some(()))]
fn validate_namespace_access(
#[case] caller: UserId,
#[case] namespace_support: NamespaceSupport,
#[case] target: Option<UserId>,
#[case] role: Option<UserRole>,
#[case] result: Option<()>,
) -> TestResult {
if result.is_some() {
assert!(
caller
.validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
.is_ok()
);
} else {
assert!(
caller
.validate_namespace_access(namespace_support, target.as_ref(), role.as_ref())
.is_err()
)
}
Ok(())
}
}