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