soaprs_auth/
credential.rs1use std::fmt;
4
5use soaprs_core::{SoapError, SoapResult};
6
7use crate::AuthorizationName;
8
9#[derive(Clone, PartialEq, Eq)]
11pub struct SecretString(String);
12
13impl SecretString {
14 pub fn new(value: impl Into<String>) -> SoapResult<Self> {
16 let value = value.into();
17 if value.is_empty() {
18 return Err(SoapError::validation("credential secret cannot be empty"));
19 }
20 Ok(Self(value))
21 }
22
23 pub fn expose_secret(&self) -> &str {
25 &self.0
26 }
27}
28
29impl fmt::Debug for SecretString {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 formatter.write_str("SecretString([REDACTED])")
32 }
33}
34
35impl fmt::Display for SecretString {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 formatter.write_str("[REDACTED]")
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum CredentialKind {
44 Bearer,
46 Password,
48 ApiKey,
50 Session,
52 Custom(AuthorizationName),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Credential {
59 strategy: AuthorizationName,
60 kind: CredentialKind,
61 identifier: Option<String>,
62 secret: SecretString,
63}
64
65impl Credential {
66 pub fn bearer(strategy: impl Into<String>, token: impl Into<String>) -> SoapResult<Self> {
68 Self::new(strategy, CredentialKind::Bearer, None, token)
69 }
70
71 pub fn api_key(
73 strategy: impl Into<String>,
74 identifier: Option<String>,
75 key: impl Into<String>,
76 ) -> SoapResult<Self> {
77 Self::new(strategy, CredentialKind::ApiKey, identifier, key)
78 }
79
80 pub fn password(
82 strategy: impl Into<String>,
83 username: impl Into<String>,
84 password: impl Into<String>,
85 ) -> SoapResult<Self> {
86 Self::new(
87 strategy,
88 CredentialKind::Password,
89 Some(username.into()),
90 password,
91 )
92 }
93
94 pub fn session(strategy: impl Into<String>, session_id: impl Into<String>) -> SoapResult<Self> {
96 Self::new(strategy, CredentialKind::Session, None, session_id)
97 }
98
99 pub fn custom(
101 strategy: impl Into<String>,
102 kind: impl Into<String>,
103 identifier: Option<String>,
104 secret: impl Into<String>,
105 ) -> SoapResult<Self> {
106 Self::new(
107 strategy,
108 CredentialKind::Custom(AuthorizationName::new(kind)?),
109 identifier,
110 secret,
111 )
112 }
113
114 fn new(
115 strategy: impl Into<String>,
116 kind: CredentialKind,
117 identifier: Option<String>,
118 secret: impl Into<String>,
119 ) -> SoapResult<Self> {
120 if identifier.as_ref().is_some_and(|identifier| {
121 identifier.is_empty() || identifier.chars().any(char::is_control)
122 }) {
123 return Err(SoapError::validation("invalid credential identifier"));
124 }
125 Ok(Self {
126 strategy: AuthorizationName::new(strategy)?,
127 kind,
128 identifier,
129 secret: SecretString::new(secret)?,
130 })
131 }
132
133 pub fn strategy(&self) -> &AuthorizationName {
135 &self.strategy
136 }
137
138 pub const fn kind(&self) -> &CredentialKind {
140 &self.kind
141 }
142
143 pub fn identifier(&self) -> Option<&str> {
145 self.identifier.as_deref()
146 }
147
148 pub const fn secret(&self) -> &SecretString {
150 &self.secret
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::Credential;
157
158 #[test]
159 fn credential_diagnostics_never_expose_secrets() {
160 let Some(credential) = Credential::bearer("jwt", "secret-token").ok() else {
161 panic!("valid credential");
162 };
163 assert!(!format!("{credential:?}").contains("secret-token"));
164 assert_eq!(credential.secret().to_string(), "[REDACTED]");
165 assert_eq!(credential.secret().expose_secret(), "secret-token");
166 }
167}