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