scim_server/multi_tenant/
adapter.rs1use crate::resource::{ListQuery, RequestContext, Resource, ResourceProvider, TenantContext};
10use serde_json::Value;
11use std::future::Future;
12
13#[derive(Debug, thiserror::Error)]
15pub enum AdapterError<E> {
16 #[error("Provider error: {0}")]
18 Provider(#[source] E),
19
20 #[error("Tenant validation error: {message}")]
22 TenantValidation { message: String },
23
24 #[error("Context conversion error: {message}")]
26 ContextConversion { message: String },
27}
28
29pub struct TenantValidatingProvider<P> {
34 inner: P,
35}
36
37impl<P> TenantValidatingProvider<P> {
38 pub fn new(provider: P) -> Self {
40 Self { inner: provider }
41 }
42
43 pub fn inner(&self) -> &P {
45 &self.inner
46 }
47
48 pub fn into_inner(self) -> P {
50 self.inner
51 }
52}
53
54impl<P> ResourceProvider for TenantValidatingProvider<P>
55where
56 P: ResourceProvider + Send + Sync,
57 P::Error: Send + Sync + 'static,
58{
59 type Error = AdapterError<P::Error>;
60
61 fn create_resource(
62 &self,
63 resource_type: &str,
64 data: Value,
65 context: &RequestContext,
66 ) -> impl Future<Output = Result<Resource, Self::Error>> + Send {
67 async move {
68 self.validate_context_consistency(context)?;
70
71 self.inner
72 .create_resource(resource_type, data, context)
73 .await
74 .map_err(AdapterError::Provider)
75 }
76 }
77
78 fn get_resource(
79 &self,
80 resource_type: &str,
81 id: &str,
82 context: &RequestContext,
83 ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send {
84 async move {
85 self.validate_context_consistency(context)?;
86
87 self.inner
88 .get_resource(resource_type, id, context)
89 .await
90 .map_err(AdapterError::Provider)
91 }
92 }
93
94 fn update_resource(
95 &self,
96 resource_type: &str,
97 id: &str,
98 data: Value,
99 context: &RequestContext,
100 ) -> impl Future<Output = Result<Resource, Self::Error>> + Send {
101 async move {
102 self.validate_context_consistency(context)?;
103
104 self.inner
105 .update_resource(resource_type, id, data, context)
106 .await
107 .map_err(AdapterError::Provider)
108 }
109 }
110
111 fn delete_resource(
112 &self,
113 resource_type: &str,
114 id: &str,
115 context: &RequestContext,
116 ) -> impl Future<Output = Result<(), Self::Error>> + Send {
117 async move {
118 self.validate_context_consistency(context)?;
119
120 self.inner
121 .delete_resource(resource_type, id, context)
122 .await
123 .map_err(AdapterError::Provider)
124 }
125 }
126
127 fn list_resources(
128 &self,
129 resource_type: &str,
130 query: Option<&ListQuery>,
131 context: &RequestContext,
132 ) -> impl Future<Output = Result<Vec<Resource>, Self::Error>> + Send {
133 async move {
134 self.validate_context_consistency(context)?;
135
136 self.inner
137 .list_resources(resource_type, query, context)
138 .await
139 .map_err(AdapterError::Provider)
140 }
141 }
142
143 fn find_resource_by_attribute(
144 &self,
145 resource_type: &str,
146 attribute: &str,
147 value: &Value,
148 context: &RequestContext,
149 ) -> impl Future<Output = Result<Option<Resource>, Self::Error>> + Send {
150 async move {
151 self.validate_context_consistency(context)?;
152
153 self.inner
154 .find_resource_by_attribute(resource_type, attribute, value, context)
155 .await
156 .map_err(AdapterError::Provider)
157 }
158 }
159
160 fn resource_exists(
161 &self,
162 resource_type: &str,
163 id: &str,
164 context: &RequestContext,
165 ) -> impl Future<Output = Result<bool, Self::Error>> + Send {
166 async move {
167 self.validate_context_consistency(context)?;
168
169 self.inner
170 .resource_exists(resource_type, id, context)
171 .await
172 .map_err(AdapterError::Provider)
173 }
174 }
175}
176
177impl<P> TenantValidatingProvider<P>
180where
181 P: ResourceProvider,
182{
183 fn validate_context_consistency(
185 &self,
186 context: &RequestContext,
187 ) -> Result<(), AdapterError<P::Error>> {
188 if context.request_id.trim().is_empty() {
190 return Err(AdapterError::ContextConversion {
191 message: "Request ID cannot be empty".to_string(),
192 });
193 }
194
195 if let Some(tenant_context) = &context.tenant_context {
197 if tenant_context.tenant_id.trim().is_empty() {
198 return Err(AdapterError::TenantValidation {
199 message: "Tenant ID cannot be empty".to_string(),
200 });
201 }
202 }
203
204 Ok(())
205 }
206}
207
208pub trait ToSingleTenant<P> {
212 fn to_single_tenant(self) -> TenantValidatingProvider<P>;
214}
215
216impl<P> ToSingleTenant<P> for P
217where
218 P: ResourceProvider,
219{
220 fn to_single_tenant(self) -> TenantValidatingProvider<P> {
221 TenantValidatingProvider::new(self)
222 }
223}
224
225pub type SingleTenantAdapter<P> = TenantValidatingProvider<P>;
229
230pub struct ContextConverter;
232
233impl ContextConverter {
234 pub fn single_tenant_context(request_id: Option<String>) -> RequestContext {
236 match request_id {
237 Some(id) => RequestContext::new(id),
238 None => RequestContext::with_generated_id(),
239 }
240 }
241
242 pub fn multi_tenant_context(
244 tenant_id: String,
245 client_id: Option<String>,
246 request_id: Option<String>,
247 ) -> RequestContext {
248 let tenant_context = TenantContext {
249 tenant_id,
250 client_id: client_id.unwrap_or_else(|| "default-client".to_string()),
251 permissions: Default::default(),
252 isolation_level: Default::default(),
253 };
254
255 match request_id {
256 Some(id) => RequestContext::with_tenant(id, tenant_context),
257 None => RequestContext::with_tenant_generated_id(tenant_context),
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[derive(Debug, thiserror::Error)]
267 #[error("Mock error")]
268 struct MockError;
269
270 struct MockProvider;
271
272 impl ResourceProvider for MockProvider {
273 type Error = MockError;
274
275 async fn create_resource(
276 &self,
277 _resource_type: &str,
278 _data: Value,
279 _context: &RequestContext,
280 ) -> Result<Resource, Self::Error> {
281 Err(MockError)
282 }
283
284 async fn get_resource(
285 &self,
286 _resource_type: &str,
287 _id: &str,
288 _context: &RequestContext,
289 ) -> Result<Option<Resource>, Self::Error> {
290 Ok(None)
291 }
292
293 async fn update_resource(
294 &self,
295 _resource_type: &str,
296 _id: &str,
297 _data: Value,
298 _context: &RequestContext,
299 ) -> Result<Resource, Self::Error> {
300 Err(MockError)
301 }
302
303 async fn delete_resource(
304 &self,
305 _resource_type: &str,
306 _id: &str,
307 _context: &RequestContext,
308 ) -> Result<(), Self::Error> {
309 Ok(())
310 }
311
312 async fn list_resources(
313 &self,
314 _resource_type: &str,
315 _query: Option<&ListQuery>,
316 _context: &RequestContext,
317 ) -> Result<Vec<Resource>, Self::Error> {
318 Ok(vec![])
319 }
320
321 async fn find_resource_by_attribute(
322 &self,
323 _resource_type: &str,
324 _attribute: &str,
325 _value: &Value,
326 _context: &RequestContext,
327 ) -> Result<Option<Resource>, Self::Error> {
328 Ok(None)
329 }
330
331 async fn resource_exists(
332 &self,
333 _resource_type: &str,
334 _id: &str,
335 _context: &RequestContext,
336 ) -> Result<bool, Self::Error> {
337 Ok(false)
338 }
339 }
340
341 #[tokio::test]
342 async fn test_validating_provider() {
343 let provider = MockProvider;
344 let validating_provider = TenantValidatingProvider::new(provider);
345
346 let context = RequestContext::with_generated_id();
347 let result = validating_provider
348 .get_resource("User", "123", &context)
349 .await;
350
351 assert!(result.is_ok());
352 }
353
354 #[tokio::test]
355 async fn test_context_validation() {
356 let provider = MockProvider;
357 let validating_provider = TenantValidatingProvider::new(provider);
358
359 let context = RequestContext::new("".to_string());
361 let result = validating_provider
362 .get_resource("User", "123", &context)
363 .await;
364
365 assert!(result.is_err());
366 assert!(matches!(
367 result.unwrap_err(),
368 AdapterError::ContextConversion { .. }
369 ));
370 }
371
372 #[test]
373 fn test_context_converter() {
374 let context = ContextConverter::single_tenant_context(Some("req-123".to_string()));
376 assert_eq!(context.request_id, "req-123");
377 assert!(context.tenant_context.is_none());
378
379 let context = ContextConverter::multi_tenant_context(
381 "tenant-1".to_string(),
382 Some("client-1".to_string()),
383 Some("req-456".to_string()),
384 );
385 assert_eq!(context.request_id, "req-456");
386 assert!(context.tenant_context.is_some());
387 assert_eq!(context.tenant_id(), Some("tenant-1"));
388 }
389
390 #[test]
391 fn test_to_single_tenant_trait() {
392 let provider = MockProvider;
393 let _validating_provider = provider.to_single_tenant();
394 }
395}