use osproxy_core::FieldName;
use serde_json::Value as JsonValue;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct JsonPath(String);
impl JsonPath {
pub fn new(path: impl Into<String>) -> Self {
Self(path.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn segments(&self) -> impl Iterator<Item = &str> {
self.0.split('.')
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PartitionKeySpec {
BodyField(JsonPath),
Header(String),
PrincipalAttr(String),
AnyOf(Vec<PartitionKeySpec>),
}
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PartitionKeySpecKind {
BodyField,
Header,
PrincipalAttr,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DocIdRule {
pub template: IdTemplate,
pub set_routing: bool,
}
impl DocIdRule {
#[must_use]
pub fn new(template: IdTemplate) -> Self {
Self {
template,
set_routing: false,
}
}
#[must_use]
pub fn with_routing(mut self, set_routing: bool) -> Self {
self.set_routing = set_routing;
self
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct IdTemplate(String);
impl IdTemplate {
pub fn new(template: impl Into<String>) -> Self {
Self(template.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn references_partition(&self) -> bool {
self.0.contains("{partition}")
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct InjectedField {
pub name: FieldName,
pub value: InjectedValue,
}
impl InjectedField {
#[must_use]
pub fn new(name: FieldName, value: InjectedValue) -> Self {
Self { name, value }
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum InjectedValue {
PartitionId,
Constant(JsonValue),
FromPrincipal(String),
FromHeader(String),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SensitivitySpec {
safe: Vec<FieldName>,
deny_by_default: bool,
}
impl Default for SensitivitySpec {
fn default() -> Self {
Self::all_sensitive()
}
}
impl SensitivitySpec {
#[must_use]
pub fn all_sensitive() -> Self {
Self {
safe: Vec::new(),
deny_by_default: true,
}
}
#[must_use]
pub fn allowing(safe: Vec<FieldName>) -> Self {
Self {
safe,
deny_by_default: true,
}
}
#[must_use]
pub fn nothing_sensitive() -> Self {
Self {
safe: Vec::new(),
deny_by_default: false,
}
}
#[must_use]
pub fn none() -> Self {
Self::nothing_sensitive()
}
#[must_use]
pub fn is_sensitive(&self, field: &FieldName) -> bool {
if self.deny_by_default {
!self.safe.contains(field)
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_path_splits_into_segments() {
assert_eq!(
JsonPath::new("a.b.c").segments().collect::<Vec<_>>(),
["a", "b", "c"]
);
assert_eq!(
JsonPath::new("flat").segments().collect::<Vec<_>>(),
["flat"]
);
}
#[test]
fn id_template_detects_partition_reference() {
assert!(IdTemplate::new("{partition}:{body.k}").references_partition());
assert!(!IdTemplate::new("{body.k}").references_partition());
}
#[test]
fn sensitivity_is_deny_by_default_with_an_allow_list() {
let spec = SensitivitySpec::allowing(vec![FieldName::from("status")]);
assert!(
spec.is_sensitive(&FieldName::from("ssn")),
"unknown ⇒ sensitive"
);
assert!(spec.is_sensitive(&FieldName::from("brand_new_field")));
assert!(
!spec.is_sensitive(&FieldName::from("status")),
"explicitly allow-listed ⇒ safe"
);
assert!(SensitivitySpec::all_sensitive().is_sensitive(&FieldName::from("anything")));
assert_eq!(SensitivitySpec::default(), SensitivitySpec::all_sensitive());
assert!(!SensitivitySpec::nothing_sensitive().is_sensitive(&FieldName::from("ssn")));
assert!(!SensitivitySpec::none().is_sensitive(&FieldName::from("ssn")));
}
}