greentic_session/
redis_store.rs1use crate::error::{invalid_argument, not_found, redis_error, serde_error};
2use crate::store::SessionStore;
3use greentic_types::{GResult, SessionData, SessionKey, TenantCtx, UserId};
4use redis::{Commands, Connection};
5use uuid::Uuid;
6
7const DEFAULT_NAMESPACE: &str = "greentic:session";
8
9pub struct RedisSessionStore {
11 client: redis::Client,
12 namespace: String,
13}
14
15impl RedisSessionStore {
16 pub fn new(client: redis::Client) -> Self {
18 Self::with_namespace(client, DEFAULT_NAMESPACE)
19 }
20
21 pub fn with_namespace(client: redis::Client, namespace: impl Into<String>) -> Self {
23 Self {
24 client,
25 namespace: namespace.into(),
26 }
27 }
28
29 fn conn(&self) -> GResult<Connection> {
30 self.client.get_connection().map_err(redis_error)
31 }
32
33 fn session_entry_key(&self, key: &SessionKey) -> String {
34 format!("{}:session:{}", self.namespace, key.as_str())
35 }
36
37 fn user_lookup_key(&self, ctx: &TenantCtx, user: &UserId) -> String {
38 let team = ctx
39 .team_id
40 .as_ref()
41 .or(ctx.team.as_ref())
42 .map(|v| v.as_str())
43 .unwrap_or("-");
44 format!(
45 "{}:user:{}:{}:{}:{}",
46 self.namespace,
47 ctx.env.as_str(),
48 ctx.tenant_id.as_str(),
49 team,
50 user.as_str()
51 )
52 }
53
54 fn ensure_alignment(ctx: &TenantCtx, data: &SessionData) -> GResult<()> {
55 if ctx.env != data.tenant_ctx.env || ctx.tenant_id != data.tenant_ctx.tenant_id {
56 return Err(invalid_argument(
57 "session data tenant context does not match provided TenantCtx",
58 ));
59 }
60 Ok(())
61 }
62
63 fn serialize(data: &SessionData) -> GResult<String> {
64 serde_json::to_string(data).map_err(serde_error)
65 }
66
67 fn deserialize(payload: String) -> GResult<SessionData> {
68 serde_json::from_str(&payload).map_err(serde_error)
69 }
70
71 fn mapping_sources<'a>(
72 ctx_hint: Option<&'a TenantCtx>,
73 data: &'a SessionData,
74 ) -> Option<(&'a TenantCtx, UserId)> {
75 if let Some(user) = data
76 .tenant_ctx
77 .user_id
78 .clone()
79 .or_else(|| data.tenant_ctx.user.clone())
80 {
81 Some((&data.tenant_ctx, user))
82 } else {
83 ctx_hint.and_then(|ctx| {
84 ctx.user_id
85 .clone()
86 .or_else(|| ctx.user.clone())
87 .map(|user| (ctx, user))
88 })
89 }
90 }
91
92 fn store_user_mapping(
93 &self,
94 conn: &mut Connection,
95 ctx_hint: Option<&TenantCtx>,
96 data: &SessionData,
97 key: &SessionKey,
98 ) -> GResult<()> {
99 if let Some((ctx, user)) = Self::mapping_sources(ctx_hint, data) {
100 let lookup_key = self.user_lookup_key(ctx, &user);
101 conn.set::<_, _, ()>(lookup_key, key.as_str())
102 .map_err(redis_error)?;
103 }
104 Ok(())
105 }
106
107 fn remove_user_mapping(
108 &self,
109 conn: &mut Connection,
110 data: &SessionData,
111 key: &SessionKey,
112 ) -> GResult<()> {
113 if let Some((ctx, user)) = Self::mapping_sources(None, data) {
114 let lookup_key = self.user_lookup_key(ctx, &user);
115 let stored: Option<String> = conn.get(&lookup_key).map_err(redis_error)?;
116 if stored
117 .as_deref()
118 .map(|value| value == key.as_str())
119 .unwrap_or(false)
120 {
121 let _: () = conn.del(lookup_key).map_err(redis_error)?;
122 }
123 }
124 Ok(())
125 }
126}
127
128impl SessionStore for RedisSessionStore {
129 fn create_session(&self, ctx: &TenantCtx, data: SessionData) -> GResult<SessionKey> {
130 Self::ensure_alignment(ctx, &data)?;
131 let key = SessionKey::new(Uuid::new_v4().to_string());
132 let payload = Self::serialize(&data)?;
133 let mut conn = self.conn()?;
134 conn.set::<_, _, ()>(self.session_entry_key(&key), payload)
135 .map_err(redis_error)?;
136 self.store_user_mapping(&mut conn, Some(ctx), &data, &key)?;
137 Ok(key)
138 }
139
140 fn get_session(&self, key: &SessionKey) -> GResult<Option<SessionData>> {
141 let mut conn = self.conn()?;
142 let payload: Option<String> = conn.get(self.session_entry_key(key)).map_err(redis_error)?;
143 payload.map(Self::deserialize).transpose()
144 }
145
146 fn update_session(&self, key: &SessionKey, data: SessionData) -> GResult<()> {
147 let mut conn = self.conn()?;
148 let entry_key = self.session_entry_key(key);
149 let existing: Option<String> = conn.get(&entry_key).map_err(redis_error)?;
150 let Some(existing_payload) = existing else {
151 return Err(not_found(key));
152 };
153 let previous = Self::deserialize(existing_payload)?;
154 let payload = Self::serialize(&data)?;
155 conn.set::<_, _, ()>(&entry_key, payload)
156 .map_err(redis_error)?;
157 self.remove_user_mapping(&mut conn, &previous, key)?;
158 self.store_user_mapping(&mut conn, None, &data, key)
159 }
160
161 fn remove_session(&self, key: &SessionKey) -> GResult<()> {
162 let mut conn = self.conn()?;
163 let entry_key = self.session_entry_key(key);
164 let existing: Option<String> = conn.get(&entry_key).map_err(redis_error)?;
165 let Some(payload) = existing else {
166 return Err(not_found(key));
167 };
168 let data = Self::deserialize(payload)?;
169 let _: () = conn.del(entry_key).map_err(redis_error)?;
170 self.remove_user_mapping(&mut conn, &data, key)
171 }
172
173 fn find_by_user(
174 &self,
175 ctx: &TenantCtx,
176 user: &UserId,
177 ) -> GResult<Option<(SessionKey, SessionData)>> {
178 let mut conn = self.conn()?;
179 let lookup_key = self.user_lookup_key(ctx, user);
180 let stored: Option<String> = conn.get(&lookup_key).map_err(redis_error)?;
181 let Some(raw_key) = stored else {
182 return Ok(None);
183 };
184 let session_key = SessionKey::new(raw_key);
185 match self.get_session(&session_key)? {
186 Some(data) => Ok(Some((session_key, data))),
187 None => {
188 let _: () = conn.del(&lookup_key).map_err(redis_error)?;
189 Ok(None)
190 }
191 }
192 }
193}