krabka_security/
mechanism.rs1use serde::{Deserialize, Serialize};
2
3use crate::AuthError;
4
5#[derive(
6 Debug,
7 Clone,
8 Copy,
9 PartialEq,
10 Eq,
11 Hash,
12 Serialize,
13 Deserialize,
14 strum::IntoStaticStr,
15 strum::EnumString,
16)]
17pub enum SaslMechanism {
18 #[strum(serialize = "PLAIN")]
19 Plain,
20 #[strum(serialize = "SCRAM-SHA-256")]
21 ScramSha256,
22 #[strum(serialize = "SCRAM-SHA-512")]
23 ScramSha512,
24 #[strum(serialize = "OAUTHBEARER")]
27 OAuthBearer,
28 #[strum(serialize = "GSSAPI")]
36 Gssapi,
37}
38
39impl SaslMechanism {
40 #[must_use]
41 pub fn wire_name(self) -> &'static str {
42 self.into()
43 }
44
45 #[must_use]
46 pub fn from_wire(s: &str) -> Option<Self> {
47 s.parse().ok()
48 }
49
50 #[must_use]
55 pub fn is_scram(self) -> bool {
56 matches!(self, Self::ScramSha256 | Self::ScramSha512)
57 }
58
59 pub const ALL: [Self; 5] = [
61 Self::Plain,
62 Self::ScramSha256,
63 Self::ScramSha512,
64 Self::OAuthBearer,
65 Self::Gssapi,
66 ];
67
68 #[must_use]
73 pub const fn is_available(self) -> bool {
74 match self {
75 Self::Gssapi => cfg!(all(feature = "gssapi", not(target_family = "wasm"))),
76 Self::Plain | Self::ScramSha256 | Self::ScramSha512 | Self::OAuthBearer => true,
77 }
78 }
79
80 pub fn available() -> impl Iterator<Item = Self> {
82 Self::ALL.into_iter().filter(|m| m.is_available())
83 }
84
85 pub const fn require_available(self) -> Result<Self, AuthError> {
94 if self.is_available() {
95 Ok(self)
96 } else {
97 Err(AuthError::UnsupportedMechanism)
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use assert2::check;
105
106 use super::*;
107
108 #[test]
109 fn wire_name_round_trip() {
110 for m in [
111 SaslMechanism::Plain,
112 SaslMechanism::ScramSha256,
113 SaslMechanism::ScramSha512,
114 SaslMechanism::OAuthBearer,
115 ] {
116 assert2::assert!(SaslMechanism::from_wire(m.wire_name()) == Some(m));
117 }
118 }
119
120 #[test]
121 fn from_wire_unknown_returns_none() {
122 for wire in ["SCRAM-SHA-128", "OAUTH", ""] {
123 assert2::assert!(SaslMechanism::from_wire(wire) == None);
124 }
125 }
126
127 #[test]
132 fn from_wire_rejects_variant_names_and_casing() {
133 for wire in [
134 "Plain",
135 "plain",
136 "ScramSha256",
137 "scram-sha-256",
138 "OAuthBearer",
139 ] {
140 assert2::assert!(SaslMechanism::from_wire(wire) == None);
141 }
142 }
143
144 #[test]
145 fn oauthbearer_wire_round_trip() {
146 check!(
147 (
148 SaslMechanism::from_wire("OAUTHBEARER"),
149 SaslMechanism::OAuthBearer.wire_name(),
150 SaslMechanism::OAuthBearer.is_scram(),
151 ) == (Some(SaslMechanism::OAuthBearer), "OAUTHBEARER", false)
152 );
153 }
154
155 #[test]
156 fn gssapi_mechanism_roundtrips_wire_name() {
157 use std::str::FromStr;
158 assert2::assert!(
159 (
160 SaslMechanism::from_str("GSSAPI").unwrap(),
161 SaslMechanism::Gssapi.wire_name(),
162 ) == (SaslMechanism::Gssapi, "GSSAPI")
163 );
164 }
165
166 const GSSAPI: bool = cfg!(all(feature = "gssapi", not(target_family = "wasm")));
168
169 #[test]
170 fn availability_follows_the_gssapi_feature() {
171 let available = SaslMechanism::ALL.map(|m| (m, m.is_available()));
172 let listed: Vec<_> = SaslMechanism::available().collect();
173 let want_listed: Vec<_> = SaslMechanism::ALL
174 .into_iter()
175 .filter(|m| *m != SaslMechanism::Gssapi || GSSAPI)
176 .collect();
177 assert2::assert!(
178 (available, listed)
179 == (
180 [
181 (SaslMechanism::Plain, true),
182 (SaslMechanism::ScramSha256, true),
183 (SaslMechanism::ScramSha512, true),
184 (SaslMechanism::OAuthBearer, true),
185 (SaslMechanism::Gssapi, GSSAPI),
186 ],
187 want_listed,
188 )
189 );
190 }
191
192 #[test]
193 fn require_available_rejects_gssapi_without_the_feature() {
194 let want_gssapi = GSSAPI
195 .then_some(SaslMechanism::Gssapi)
196 .ok_or(AuthError::UnsupportedMechanism);
197 assert2::assert!(
198 (
199 SaslMechanism::Gssapi.require_available(),
200 SaslMechanism::Plain.require_available(),
201 ) == (want_gssapi, Ok(SaslMechanism::Plain))
202 );
203 }
204
205 #[test]
206 fn is_scram_predicate() {
207 for (_case, mechanism, want) in [
208 ("PLAIN", SaslMechanism::Plain, false),
209 ("SCRAM SHA-256", SaslMechanism::ScramSha256, true),
210 ("SCRAM SHA-512", SaslMechanism::ScramSha512, true),
211 ] {
212 assert2::assert!(mechanism.is_scram() == want);
213 }
214 }
215}