Files
mnote/rust/crates/core-protocol/src/governance.rs
T

117 lines
2.9 KiB
Rust
Raw Normal View History

2026-04-14 13:22:29 +08:00
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessContext {
pub tenant_id: Option<String>,
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<String>,
job_type: impl Into<String>,
request_id: impl Into<String>,
trace_id: impl Into<String>,
workspace_id: impl Into<String>,
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);
}
}