use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PermissionState {
#[default]
Draft,
Candidate,
Active,
Rejected,
}
impl PermissionState {
pub const fn is_active(self) -> bool {
matches!(self, PermissionState::Active)
}
pub const fn is_candidate(self) -> bool {
matches!(self, PermissionState::Candidate)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Effect {
#[default]
Allow,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SubjectType {
User,
Role,
Rule,
LlmAgent,
#[default]
Any,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Subject {
#[serde(default)]
pub subject_type: SubjectType,
#[serde(default)]
pub id: String,
}
impl Subject {
pub fn any() -> Self {
Self {
subject_type: SubjectType::Any,
id: String::new(),
}
}
pub fn human(name: impl Into<String>) -> Self {
Self {
subject_type: SubjectType::User,
id: name.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ResourceType {
Fact,
IoAction,
Api,
#[default]
Shared,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Resource {
#[serde(default)]
pub resource_type: ResourceType,
#[serde(default)]
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Scope {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PermissionEntry {
pub id: String,
#[serde(default)]
pub version: u64,
#[serde(default)]
pub state: PermissionState,
#[serde(default)]
pub subject: Subject,
#[serde(default)]
pub resource: Resource,
#[serde(default = "default_action")]
pub action: String,
#[serde(default)]
pub effect: Effect,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conditions: Option<serde_json::Value>,
#[serde(default)]
pub scope: Scope,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
#[serde(default = "default_updated_by")]
pub updated_by: String,
}
fn default_action() -> String {
"*".to_string()
}
fn default_updated_by() -> String {
"unknown".to_string()
}
impl PermissionEntry {
pub fn new(id: impl Into<String>, subject: Subject, resource: Resource, effect: Effect) -> Self {
Self {
id: id.into(),
version: 1,
state: PermissionState::Draft,
subject,
resource,
action: default_action(),
effect,
conditions: None,
scope: Scope { tenant_id: None },
cause: None,
updated_by: default_updated_by(),
}
}
pub fn submit(&mut self) -> Result<(), PermissionError> {
self.state = match self.state {
PermissionState::Draft => PermissionState::Candidate,
other => {
return Err(PermissionError::InvalidState(format!(
"only Draft can be submitted, current = {other:?}"
)))
}
};
Ok(())
}
pub fn review(&mut self, approve: bool) -> Result<(), PermissionError> {
match self.state {
PermissionState::Candidate => {
self.state = if approve {
PermissionState::Active
} else {
PermissionState::Rejected
};
Ok(())
}
other => Err(PermissionError::InvalidState(format!(
"only Candidate can be reviewed, current = {other:?}"
))),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PermissionError {
#[error("permission entry not found: {0}")]
NotFound(String),
#[error("invalid permission state: {0}")]
InvalidState(String),
#[error("condition evaluation failed: {0}")]
Condition(String),
#[error("permission serialization error: {0}")]
Json(#[from] serde_json::Error),
#[error("permission store error: {0}")]
Store(String),
}