use serde::{Deserialize, Serialize};
use super::identity::TenantId;
use super::tenant_isolation::TenantBoundary;
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[repr(u8)]
pub enum SecurityClassification {
#[default]
Public = 0,
Internal = 1,
Confidential = 2,
Restricted = 3,
TopSecret = 4,
}
impl SecurityClassification {
#[must_use]
pub const fn rank(self) -> u8 {
self as u8
}
#[must_use]
pub const fn may_flow_to(self, destination: Self) -> bool {
self.rank() <= destination.rank()
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"public" => Some(Self::Public),
"internal" => Some(Self::Internal),
"confidential" => Some(Self::Confidential),
"restricted" => Some(Self::Restricted),
"topsecret" | "top_secret" | "top-secret" => Some(Self::TopSecret),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct ResidencyRequirement {
#[serde(default)]
pub allowed_regions: Vec<String>,
#[serde(default)]
pub prohibited_regions: Vec<String>,
#[serde(default)]
pub data_sovereign: bool,
}
impl ResidencyRequirement {
#[must_use]
pub fn unrestricted() -> Self {
Self::default()
}
#[must_use]
pub fn allowed_in<I, S>(regions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
allowed_regions: regions.into_iter().map(Into::into).collect(),
..Self::default()
}
}
#[must_use]
pub fn sovereign<I, S>(regions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
allowed_regions: regions.into_iter().map(Into::into).collect(),
data_sovereign: true,
..Self::default()
}
}
#[must_use]
pub fn with_allowed_region(mut self, region: impl Into<String>) -> Self {
self.allowed_regions.push(region.into());
self
}
#[must_use]
pub fn with_prohibited_region(mut self, region: impl Into<String>) -> Self {
self.prohibited_regions.push(region.into());
self
}
#[must_use]
pub const fn with_sovereignty(mut self, sovereign: bool) -> Self {
self.data_sovereign = sovereign;
self
}
#[must_use]
pub fn allows_region(&self, region: &str) -> bool {
let region = region.trim();
if region.is_empty() || (self.data_sovereign && self.allowed_regions.is_empty()) {
return false;
}
let prohibited = self
.prohibited_regions
.iter()
.any(|candidate| region_matches(candidate, region));
if prohibited {
return false;
}
self.allowed_regions.is_empty()
|| self
.allowed_regions
.iter()
.any(|candidate| region_matches(candidate, region))
}
pub fn validate(&self) -> Result<(), String> {
if self.data_sovereign && self.allowed_regions.is_empty() {
return Err("sovereign residency requires at least one allowed region".to_owned());
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ClassifiedData<T> {
pub data: T,
pub classification: SecurityClassification,
pub residency: ResidencyRequirement,
}
impl<T> ClassifiedData<T> {
#[must_use]
pub fn new(
data: T,
classification: SecurityClassification,
residency: ResidencyRequirement,
) -> Self {
Self {
data,
classification,
residency,
}
}
#[must_use]
pub const fn as_ref(&self) -> &T {
&self.data
}
#[must_use]
pub fn map<U>(self, map: impl FnOnce(T) -> U) -> ClassifiedData<U> {
ClassifiedData {
data: map(self.data),
classification: self.classification,
residency: self.residency,
}
}
#[must_use]
pub fn into_inner(self) -> T {
self.data
}
#[must_use]
pub fn is_allowed_in(&self, region: &str) -> bool {
self.residency.allows_region(region)
}
}
impl<T: TenantBoundary> TenantBoundary for ClassifiedData<T> {
fn tenant_id(&self) -> &TenantId {
self.data.tenant_id()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ClassificationRule {
pub content_type: String,
pub classification: SecurityClassification,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ClassificationPolicy {
pub default: SecurityClassification,
#[serde(default)]
pub rules: Vec<ClassificationRule>,
}
impl Default for ClassificationPolicy {
fn default() -> Self {
Self {
default: SecurityClassification::Public,
rules: Vec::new(),
}
}
}
impl ClassificationPolicy {
#[must_use]
pub fn new(default: SecurityClassification) -> Self {
Self {
default,
rules: Vec::new(),
}
}
#[must_use]
pub fn with_rule(
mut self,
content_type: impl Into<String>,
classification: SecurityClassification,
) -> Self {
self.add_rule(content_type, classification);
self
}
pub fn add_rule(
&mut self,
content_type: impl Into<String>,
classification: SecurityClassification,
) {
self.rules.push(ClassificationRule {
content_type: content_type.into(),
classification,
});
}
#[must_use]
pub fn classify(&self, content_type: &str) -> SecurityClassification {
self.rules
.iter()
.filter(|rule| content_type_matches(&rule.content_type, content_type))
.map(|rule| rule.classification)
.max()
.unwrap_or(self.default)
}
#[must_use]
pub fn auto_classify(&self, content_type: &str) -> SecurityClassification {
self.classify(content_type)
}
}
fn region_matches(pattern: &str, value: &str) -> bool {
let pattern = pattern.trim();
pattern == "*" || pattern.eq_ignore_ascii_case(value)
}
fn content_type_matches(pattern: &str, value: &str) -> bool {
let pattern = pattern.trim();
if pattern == "*" || pattern.eq_ignore_ascii_case(value.trim()) {
return true;
}
pattern
.strip_suffix('*')
.is_some_and(|prefix| value.trim().starts_with(prefix.trim_end_matches('/')))
}