#[derive(Debug, Clone, PartialEq, Eq)] pub struct AccessContext { pub tenant_id: Option, pub workspace_id: String, pub actor_id: String, pub actor_type: String, pub source: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccessDecision { Allow, Deny(AccessDenyReason), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccessDenyReason { MissingTenant, MissingWorkspace, PermissionDenied, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum JobStatus { Pending, Running, Succeeded, Failed, RetryScheduled, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct JobTicket { pub job_id: String, pub job_type: String, pub request_id: String, pub trace_id: String, pub workspace_id: String, pub status: JobStatus, } impl JobTicket { pub fn new( job_id: impl Into, job_type: impl Into, request_id: impl Into, trace_id: impl Into, workspace_id: impl Into, status: JobStatus, ) -> Self { Self { job_id: job_id.into(), job_type: job_type.into(), request_id: request_id.into(), trace_id: trace_id.into(), workspace_id: workspace_id.into(), status, } } } pub fn decide_access(context: &AccessContext) -> AccessDecision { if context.tenant_id.is_none() { return AccessDecision::Deny(AccessDenyReason::MissingTenant); } if context.workspace_id.trim().is_empty() { return AccessDecision::Deny(AccessDenyReason::MissingWorkspace); } AccessDecision::Allow } #[cfg(test)] mod tests { use super::*; #[test] fn access_requires_tenant() { let context = AccessContext { tenant_id: None, workspace_id: "ws-1".to_string(), actor_id: "user-1".to_string(), actor_type: "human".to_string(), source: "react-next".to_string(), }; assert_eq!( decide_access(&context), AccessDecision::Deny(AccessDenyReason::MissingTenant) ); } #[test] fn access_allows_valid_context() { let context = AccessContext { tenant_id: Some("tenant-1".to_string()), workspace_id: "ws-1".to_string(), actor_id: "user-1".to_string(), actor_type: "human".to_string(), source: "react-next".to_string(), }; assert_eq!(decide_access(&context), AccessDecision::Allow); } #[test] fn job_ticket_keeps_trace_fields() { let ticket = JobTicket::new( "job-1", "rebuild-index", "req-1", "trace-1", "ws-1", JobStatus::Pending, ); assert_eq!(ticket.trace_id, "trace-1"); assert_eq!(ticket.status, JobStatus::Pending); } }