1use crate::permission::Permission;
2use crate::{AccessScope, PolicyEngineRef};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7pub struct SecurityContext {
8 tenant_id: Uuid,
9 subject_id: Uuid,
10 subject_type: Option<String>,
11 permissions: Vec<Permission>,
12 environment: Vec<(String, String)>,
13}
14
15impl SecurityContext {
16 #[must_use]
18 pub fn builder() -> SecurityContextBuilder {
19 SecurityContextBuilder::default()
20 }
21
22 #[must_use]
24 pub fn anonymous() -> Self {
25 SecurityContextBuilder::default().build()
26 }
27
28 #[must_use]
30 pub fn tenant_id(&self) -> Uuid {
31 self.tenant_id
32 }
33
34 #[must_use]
36 pub fn subject_id(&self) -> Uuid {
37 self.subject_id
38 }
39
40 #[must_use]
42 pub fn permissions(&self) -> Vec<Permission> {
43 self.permissions.clone()
44 }
45
46 #[must_use]
49 pub fn environment(&self) -> Vec<(String, String)> {
50 self.environment.clone()
51 }
52
53 pub fn scope(&self, policy_engine: PolicyEngineRef) -> AccessScopeResolver {
54 AccessScopeResolver {
55 _policy_engine: policy_engine,
56 context: self.clone(),
57 accessible_tenants: None,
58 }
59 }
60}
61
62pub struct AccessScopeResolver {
63 _policy_engine: PolicyEngineRef,
64 context: SecurityContext,
65 accessible_tenants: Option<Vec<Uuid>>,
67}
68
69impl AccessScopeResolver {
70 #[must_use]
92 pub fn include_accessible_tenants(mut self, tenants: Vec<Uuid>) -> Self {
93 self.accessible_tenants = Some(tenants);
94 self
95 }
96
97 #[must_use]
98 pub fn include_resource_ids(&self) -> &Self {
99 self
100 }
101
102 pub async fn prepare(&self) -> Result<AccessScope, Box<dyn std::error::Error>> {
107 std::future::ready(()).await;
110
111 if let Some(ref tenants) = self.accessible_tenants {
113 return Ok(AccessScope::tenants_only(tenants.clone()));
114 }
115
116 if self.context.tenant_id != Uuid::default() {
118 return Ok(AccessScope::tenants_only(vec![self.context.tenant_id]));
119 }
120
121 Ok(AccessScope::default())
123 }
124}
125
126#[derive(Default)]
127pub struct SecurityContextBuilder {
128 tenant_id: Option<Uuid>,
129 subject_id: Option<Uuid>,
130 subject_type: Option<String>,
131 permissions: Vec<Permission>,
132 environment: Vec<(String, String)>,
133}
134
135impl SecurityContextBuilder {
136 #[must_use]
137 pub fn tenant_id(mut self, tenant_id: Uuid) -> Self {
138 self.tenant_id = Some(tenant_id);
139 self
140 }
141
142 #[must_use]
143 pub fn subject_id(mut self, subject_id: Uuid) -> Self {
144 self.subject_id = Some(subject_id);
145 self
146 }
147
148 #[must_use]
149 pub fn subject_type(mut self, subject_type: &str) -> Self {
150 self.subject_type = Some(subject_type.to_owned());
151 self
152 }
153
154 #[must_use]
155 pub fn add_permission(mut self, permission: Permission) -> Self {
156 self.permissions.push(permission);
157 self
158 }
159
160 #[must_use]
161 pub fn add_environment_attribute(mut self, key: &str, value: &str) -> Self {
162 self.environment.push((key.to_owned(), value.to_owned()));
163 self
164 }
165
166 #[must_use]
167 pub fn build(self) -> SecurityContext {
168 SecurityContext {
169 tenant_id: self.tenant_id.unwrap_or_default(),
170 subject_id: self.subject_id.unwrap_or_default(),
171 subject_type: self.subject_type,
172 permissions: self.permissions,
173 environment: self.environment,
174 }
175 }
176}
177
178#[cfg(test)]
179#[cfg_attr(coverage_nightly, coverage(off))]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_security_context_builder_full() {
185 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
186 let subject_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap();
187
188 let permission1 = Permission::builder()
189 .tenant_id(tenant_id)
190 .resource_pattern("gts.x.core.events.topic.v1~*")
191 .action("publish")
192 .build()
193 .unwrap();
194
195 let permission2 = Permission::builder()
196 .resource_pattern("file-parser")
197 .action("edit")
198 .build()
199 .unwrap();
200
201 let ctx = SecurityContext::builder()
202 .tenant_id(tenant_id)
203 .subject_id(subject_id)
204 .subject_type("user")
205 .add_permission(permission1)
206 .add_permission(permission2)
207 .add_environment_attribute("ip", "192.168.1.1")
208 .add_environment_attribute("device", "mobile")
209 .build();
210
211 assert_eq!(ctx.tenant_id(), tenant_id);
212 assert_eq!(ctx.subject_id(), subject_id);
213 assert_eq!(ctx.permissions().len(), 2);
214 assert_eq!(ctx.environment().len(), 2);
215 assert_eq!(
216 ctx.environment()[0],
217 ("ip".to_owned(), "192.168.1.1".to_owned())
218 );
219 assert_eq!(
220 ctx.environment()[1],
221 ("device".to_owned(), "mobile".to_owned())
222 );
223 }
224
225 #[test]
226 fn test_security_context_builder_minimal() {
227 let ctx = SecurityContext::builder().build();
228
229 assert_eq!(ctx.tenant_id(), Uuid::default());
230 assert_eq!(ctx.subject_id(), Uuid::default());
231 assert_eq!(ctx.permissions().len(), 0);
232 assert_eq!(ctx.environment().len(), 0);
233 }
234
235 #[test]
236 fn test_security_context_builder_partial() {
237 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
238
239 let ctx = SecurityContext::builder()
240 .tenant_id(tenant_id)
241 .subject_type("service")
242 .build();
243
244 assert_eq!(ctx.tenant_id(), tenant_id);
245 assert_eq!(ctx.subject_id(), Uuid::default());
246 assert_eq!(ctx.permissions().len(), 0);
247 assert_eq!(ctx.environment().len(), 0);
248 }
249
250 #[test]
251 fn test_security_context_anonymous() {
252 let ctx = SecurityContext::anonymous();
253
254 assert_eq!(ctx.tenant_id(), Uuid::default());
255 assert_eq!(ctx.subject_id(), Uuid::default());
256 assert_eq!(ctx.permissions().len(), 0);
257 assert_eq!(ctx.environment().len(), 0);
258 }
259
260 #[test]
261 fn test_security_context_with_multiple_permissions() {
262 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
263
264 let permission1 = Permission::builder()
265 .tenant_id(tenant_id)
266 .resource_pattern("gts.x.core.events.topic.v1~vendor.*")
267 .action("publish")
268 .build()
269 .unwrap();
270
271 let permission2 = Permission::builder()
272 .tenant_id(tenant_id)
273 .resource_pattern("gts.x.core.events.topic.v1~vendor.*")
274 .action("subscribe")
275 .build()
276 .unwrap();
277
278 let permission3 = Permission::builder()
279 .resource_pattern("file-parser")
280 .action("edit")
281 .build()
282 .unwrap();
283
284 let ctx = SecurityContext::builder()
285 .tenant_id(tenant_id)
286 .add_permission(permission1)
287 .add_permission(permission2)
288 .add_permission(permission3)
289 .build();
290
291 let perms = ctx.permissions();
292 assert_eq!(perms.len(), 3);
293 assert_eq!(perms[0].tenant_id(), Some(tenant_id));
294 assert_eq!(
295 perms[0].resource_pattern(),
296 "gts.x.core.events.topic.v1~vendor.*"
297 );
298 assert_eq!(perms[0].action(), "publish");
299 assert_eq!(perms[1].action(), "subscribe");
300 assert_eq!(perms[2].resource_pattern(), "file-parser");
301 }
302
303 #[test]
304 fn test_security_context_with_multiple_environment_attributes() {
305 let ctx = SecurityContext::builder()
306 .add_environment_attribute("ip", "192.168.1.1")
307 .add_environment_attribute("device", "mobile")
308 .add_environment_attribute("location", "US")
309 .add_environment_attribute("time_zone", "PST")
310 .build();
311
312 let env = ctx.environment();
313 assert_eq!(env.len(), 4);
314 assert_eq!(env[0], ("ip".to_owned(), "192.168.1.1".to_owned()));
315 assert_eq!(env[1], ("device".to_owned(), "mobile".to_owned()));
316 assert_eq!(env[2], ("location".to_owned(), "US".to_owned()));
317 assert_eq!(env[3], ("time_zone".to_owned(), "PST".to_owned()));
318 }
319
320 #[test]
321 fn test_security_context_builder_chaining() {
322 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
323 let subject_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap();
324
325 let ctx = SecurityContext::builder()
327 .tenant_id(tenant_id)
328 .subject_id(subject_id)
329 .subject_type("user")
330 .build();
331
332 assert_eq!(ctx.tenant_id(), tenant_id);
333 assert_eq!(ctx.subject_id(), subject_id);
334 }
335
336 #[test]
337 fn test_security_context_getters_dont_mutate() {
338 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
339
340 let permission = Permission::builder()
341 .resource_pattern("file-parser")
342 .action("edit")
343 .build()
344 .unwrap();
345
346 let ctx = SecurityContext::builder()
347 .tenant_id(tenant_id)
348 .add_permission(permission)
349 .add_environment_attribute("ip", "192.168.1.1")
350 .build();
351
352 let _perms1 = ctx.permissions();
354 let perms2 = ctx.permissions();
355 assert_eq!(perms2.len(), 1);
356
357 let _env1 = ctx.environment();
358 let env2 = ctx.environment();
359 assert_eq!(env2.len(), 1);
360
361 assert_eq!(ctx.tenant_id(), tenant_id);
363 }
364
365 #[test]
366 fn test_security_context_clone() {
367 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
368 let subject_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap();
369
370 let permission = Permission::builder()
371 .resource_pattern("file-parser")
372 .action("edit")
373 .build()
374 .unwrap();
375
376 let ctx1 = SecurityContext::builder()
377 .tenant_id(tenant_id)
378 .subject_id(subject_id)
379 .add_permission(permission)
380 .add_environment_attribute("ip", "192.168.1.1")
381 .build();
382
383 let ctx2 = ctx1.clone();
384
385 assert_eq!(ctx2.tenant_id(), ctx1.tenant_id());
386 assert_eq!(ctx2.subject_id(), ctx1.subject_id());
387 assert_eq!(ctx2.permissions().len(), ctx1.permissions().len());
388 assert_eq!(ctx2.environment().len(), ctx1.environment().len());
389 }
390
391 #[test]
392 fn test_security_context_serialize_deserialize() {
393 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
394 let subject_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap();
395
396 let permission = Permission::builder()
397 .tenant_id(tenant_id)
398 .resource_pattern("gts.x.core.events.topic.v1~*")
399 .action("publish")
400 .build()
401 .unwrap();
402
403 let original = SecurityContext::builder()
404 .tenant_id(tenant_id)
405 .subject_id(subject_id)
406 .subject_type("user")
407 .add_permission(permission)
408 .add_environment_attribute("ip", "192.168.1.1")
409 .build();
410
411 let serialized = serde_json::to_string(&original).unwrap();
412 let deserialized: SecurityContext = serde_json::from_str(&serialized).unwrap();
413
414 assert_eq!(deserialized.tenant_id(), original.tenant_id());
415 assert_eq!(deserialized.subject_id(), original.subject_id());
416 assert_eq!(deserialized.permissions().len(), 1);
417 assert_eq!(deserialized.environment().len(), 1);
418 }
419
420 #[test]
421 fn test_security_context_with_no_subject_type() {
422 let tenant_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
423
424 let ctx = SecurityContext::builder().tenant_id(tenant_id).build();
425
426 assert_eq!(ctx.tenant_id(), tenant_id);
428 }
429
430 #[test]
431 fn test_security_context_empty_permissions() {
432 let ctx = SecurityContext::builder().build();
433
434 assert!(ctx.permissions().is_empty());
435 }
436}