rustauth_redis/
secondary.rs1use redis::aio::ConnectionManager;
2use redis::AsyncCommands;
3use rustauth_core::error::RustAuthError;
4use rustauth_core::options::{SecondaryStorage, SecondaryStorageFuture};
5
6use crate::url::{secondary_storage_scan_pattern, validate_key_prefix};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct RedisSecondaryStorageOptions {
10 pub key_prefix: String,
11 pub scan_count: u32,
12}
13
14impl Default for RedisSecondaryStorageOptions {
15 fn default() -> Self {
16 Self {
17 key_prefix: "rustauth:".to_owned(),
18 scan_count: 100,
19 }
20 }
21}
22
23#[derive(Clone)]
24pub struct RedisSecondaryStorage {
25 manager: ConnectionManager,
26 options: RedisSecondaryStorageOptions,
27}
28
29impl RedisSecondaryStorage {
30 pub async fn connect(redis_url: &str) -> Result<Self, RustAuthError> {
31 Self::connect_with_options(redis_url, RedisSecondaryStorageOptions::default()).await
32 }
33
34 pub async fn connect_with_options(
35 redis_url: &str,
36 options: RedisSecondaryStorageOptions,
37 ) -> Result<Self, RustAuthError> {
38 let manager = crate::connect_manager(redis_url).await?;
39 Ok(Self::new(manager, options))
40 }
41
42 pub fn new(manager: ConnectionManager, options: RedisSecondaryStorageOptions) -> Self {
43 Self { manager, options }
44 }
45
46 pub async fn list_keys(&self) -> Result<Vec<String>, RustAuthError> {
47 validate_secondary_storage_options(&self.options)?;
48 let secondary_prefix = self.secondary_prefix();
49 let pattern = secondary_storage_scan_pattern(&secondary_prefix);
50 let physical_keys = scan_keys(&self.manager, &pattern, self.options.scan_count).await?;
51 let mut keys = Vec::new();
52 for key in physical_keys {
53 if let Some(unprefixed) = key.strip_prefix(secondary_prefix.as_str()) {
54 keys.push(unprefixed.to_owned());
55 }
56 }
57 Ok(keys)
58 }
59
60 pub async fn clear(&self) -> Result<(), RustAuthError> {
61 let keys = self
62 .list_keys()
63 .await?
64 .into_iter()
65 .map(|key| self.prefixed_key(&key))
66 .collect::<Result<Vec<_>, _>>()?;
67 if keys.is_empty() {
68 return Ok(());
69 }
70 let mut manager = self.manager.clone();
71 let _: usize = manager
72 .del(keys)
73 .await
74 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
75 Ok(())
76 }
77
78 fn secondary_prefix(&self) -> String {
79 format!("{}secondary:", self.options.key_prefix)
80 }
81
82 fn prefixed_key(&self, key: &str) -> Result<String, RustAuthError> {
83 validate_key_prefix(&self.options.key_prefix)?;
84 Ok(format!("{}secondary:{key}", self.options.key_prefix))
85 }
86}
87
88impl SecondaryStorage for RedisSecondaryStorage {
89 fn get<'a>(&'a self, key: &'a str) -> SecondaryStorageFuture<'a, Option<String>> {
90 Box::pin(async move {
91 let mut manager = self.manager.clone();
92 manager
93 .get(self.prefixed_key(key)?)
94 .await
95 .map_err(|error| RustAuthError::Adapter(error.to_string()))
96 })
97 }
98
99 fn set<'a>(
100 &'a self,
101 key: &'a str,
102 value: String,
103 ttl_seconds: Option<u64>,
104 ) -> SecondaryStorageFuture<'a, ()> {
105 Box::pin(async move {
106 let redis_key = self.prefixed_key(key)?;
107 let mut manager = self.manager.clone();
108 match ttl_seconds {
109 Some(0) => {
110 let _: usize = manager
111 .del(redis_key)
112 .await
113 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
114 }
115 Some(ttl_seconds) => {
116 let _: () = manager
117 .set_ex(redis_key, value, ttl_seconds)
118 .await
119 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
120 }
121 None => {
122 let _: () = manager
123 .set(redis_key, value)
124 .await
125 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
126 }
127 }
128 Ok(())
129 })
130 }
131
132 fn set_if_not_exists<'a>(
133 &'a self,
134 key: &'a str,
135 value: String,
136 ttl_seconds: Option<u64>,
137 ) -> SecondaryStorageFuture<'a, bool> {
138 Box::pin(async move {
139 let redis_key = self.prefixed_key(key)?;
140 let mut manager = self.manager.clone();
141 if ttl_seconds == Some(0) {
142 return Ok(false);
143 }
144 let mut command = redis::cmd("SET");
145 command.arg(&redis_key).arg(value).arg("NX");
146 if let Some(ttl_seconds) = ttl_seconds {
147 command.arg("EX").arg(ttl_seconds);
148 }
149 let created: Option<String> = command
150 .query_async(&mut manager)
151 .await
152 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
153 Ok(created.is_some())
154 })
155 }
156
157 fn delete<'a>(&'a self, key: &'a str) -> SecondaryStorageFuture<'a, ()> {
158 Box::pin(async move {
159 let mut manager = self.manager.clone();
160 let _: usize = manager
161 .del(self.prefixed_key(key)?)
162 .await
163 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
164 Ok(())
165 })
166 }
167
168 fn take<'a>(&'a self, key: &'a str) -> SecondaryStorageFuture<'a, Option<String>> {
169 Box::pin(async move {
170 let mut manager = self.manager.clone();
171 redis::cmd("GETDEL")
172 .arg(self.prefixed_key(key)?)
173 .query_async(&mut manager)
174 .await
175 .map_err(|error| RustAuthError::Adapter(error.to_string()))
176 })
177 }
178
179 fn compare_and_set<'a>(
180 &'a self,
181 key: &'a str,
182 expected: Option<String>,
183 value: String,
184 ttl_seconds: Option<u64>,
185 ) -> SecondaryStorageFuture<'a, bool> {
186 Box::pin(async move {
187 let redis_key = self.prefixed_key(key)?;
188 let mut manager = self.manager.clone();
189 if ttl_seconds == Some(0) {
190 let deleted = self.delete_if_value(key, expected).await?;
191 return Ok(deleted);
192 }
193 let script = r#"
194local current = redis.call("GET", KEYS[1])
195local expected_is_nil = ARGV[1]
196local expected = ARGV[2]
197if expected_is_nil == "1" then
198 if current ~= false then return 0 end
199else
200 if current ~= expected then return 0 end
201end
202if ARGV[4] == "" then
203 redis.call("SET", KEYS[1], ARGV[3])
204else
205 redis.call("SET", KEYS[1], ARGV[3], "EX", tonumber(ARGV[4]))
206end
207return 1
208"#;
209 let expected_is_nil = expected.is_none();
210 let expected = expected.unwrap_or_default();
211 let ttl = ttl_seconds.map(|ttl| ttl.to_string()).unwrap_or_default();
212 let applied: i64 = redis::cmd("EVAL")
213 .arg(script)
214 .arg(1)
215 .arg(redis_key)
216 .arg(if expected_is_nil { "1" } else { "0" })
217 .arg(expected)
218 .arg(value)
219 .arg(ttl)
220 .query_async(&mut manager)
221 .await
222 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
223 Ok(applied == 1)
224 })
225 }
226
227 fn delete_if_value<'a>(
228 &'a self,
229 key: &'a str,
230 expected: Option<String>,
231 ) -> SecondaryStorageFuture<'a, bool> {
232 Box::pin(async move {
233 let Some(expected) = expected else {
234 return Ok(false);
235 };
236 let redis_key = self.prefixed_key(key)?;
237 let mut manager = self.manager.clone();
238 let script = r#"
239if redis.call("GET", KEYS[1]) == ARGV[1] then
240 redis.call("DEL", KEYS[1])
241 return 1
242end
243return 0
244"#;
245 let deleted: i64 = redis::cmd("EVAL")
246 .arg(script)
247 .arg(1)
248 .arg(redis_key)
249 .arg(expected)
250 .query_async(&mut manager)
251 .await
252 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
253 Ok(deleted == 1)
254 })
255 }
256}
257
258fn validate_secondary_storage_options(
259 options: &RedisSecondaryStorageOptions,
260) -> Result<(), RustAuthError> {
261 validate_key_prefix(&options.key_prefix)?;
262 if options.scan_count == 0 {
263 return Err(RustAuthError::InvalidConfig(
264 "secondary storage scan count must be greater than zero".to_owned(),
265 ));
266 }
267 Ok(())
268}
269
270async fn scan_keys(
271 manager: &ConnectionManager,
272 pattern: &str,
273 count: u32,
274) -> Result<Vec<String>, RustAuthError> {
275 let mut conn = manager.clone();
276 let mut cursor = 0u64;
277 let mut keys = Vec::new();
278 loop {
279 let (next_cursor, page): (u64, Vec<String>) = redis::cmd("SCAN")
280 .arg(cursor)
281 .arg("MATCH")
282 .arg(pattern)
283 .arg("COUNT")
284 .arg(count)
285 .query_async(&mut conn)
286 .await
287 .map_err(|error| RustAuthError::Adapter(error.to_string()))?;
288 keys.extend(page);
289 if next_cursor == 0 {
290 break;
291 }
292 cursor = next_cursor;
293 }
294 Ok(keys)
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use rustauth_core::error::RustAuthError;
301
302 #[test]
303 fn list_keys_rejects_empty_prefix() {
304 let options = RedisSecondaryStorageOptions {
305 key_prefix: String::new(),
306 scan_count: 100,
307 };
308 assert!(matches!(
309 validate_secondary_storage_options(&options),
310 Err(RustAuthError::InvalidConfig(message))
311 if message == "secondary storage key prefix must not be empty"
312 ));
313 }
314
315 #[test]
316 fn list_keys_rejects_zero_scan_count() {
317 let options = RedisSecondaryStorageOptions {
318 key_prefix: "rustauth:".to_owned(),
319 scan_count: 0,
320 };
321 assert!(matches!(
322 validate_secondary_storage_options(&options),
323 Err(RustAuthError::InvalidConfig(message))
324 if message == "secondary storage scan count must be greater than zero"
325 ));
326 }
327}