affine_core 0.0.5

AFFiNE primitive core.
Documentation
use serde_json::Value;

use super::{
  InviteAbuseAction, InviteAbuseReason, InviteAbuseStatus, InviteAbuseSubject, InviteAbuseSubjectKind,
  PersistedInviteAbuseError, PersistedInviteAbuseRecord, invite_abuse_status, normalize_domain, workspace_subject_key,
};

fn required<'a>(value: &'a Option<String>, field: &'static str) -> Result<&'a str, PersistedInviteAbuseError> {
  value.as_deref().ok_or(PersistedInviteAbuseError::Missing(field))
}

fn hash_identity(value: &str, prefix: &str, length: usize) -> bool {
  value.strip_prefix(prefix).is_some_and(|hash| {
    hash.len() == length
      && hash
        .bytes()
        .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
  })
}

fn decode_kind(value: &str) -> Result<InviteAbuseSubjectKind, PersistedInviteAbuseError> {
  InviteAbuseSubjectKind::from_persisted(value).ok_or(PersistedInviteAbuseError::Invalid("subject kind"))
}

fn decode_status(value: &str) -> Result<InviteAbuseStatus, PersistedInviteAbuseError> {
  InviteAbuseStatus::from_persisted(value).ok_or(PersistedInviteAbuseError::Invalid("subject status"))
}

fn decode_action(value: &str) -> Result<InviteAbuseAction, PersistedInviteAbuseError> {
  InviteAbuseAction::from_persisted(value).ok_or(PersistedInviteAbuseError::Invalid("subject action"))
}

fn decode_reason(value: &str) -> Result<InviteAbuseReason, PersistedInviteAbuseError> {
  InviteAbuseReason::from_persisted(value).ok_or(PersistedInviteAbuseError::Invalid("subject reason"))
}

fn validate_targets(value: &Value, requested: i32) -> Result<(), PersistedInviteAbuseError> {
  let targets = value
    .as_array()
    .ok_or(PersistedInviteAbuseError::Invalid("evidence target domains"))?;
  let mut total = 0_i32;
  for target in targets {
    let target = target
      .as_object()
      .filter(|target| target.len() == 2)
      .ok_or(PersistedInviteAbuseError::Invalid("evidence target domains"))?;
    let domain = target
      .get("domain")
      .and_then(Value::as_str)
      .ok_or(PersistedInviteAbuseError::Invalid("evidence target domains"))?;
    let count = target
      .get("count")
      .and_then(Value::as_i64)
      .and_then(|count| i32::try_from(count).ok())
      .filter(|count| *count > 0)
      .ok_or(PersistedInviteAbuseError::Invalid("evidence target domains"))?;
    if domain.is_empty() || domain != normalize_domain(domain) {
      return Err(PersistedInviteAbuseError::Invalid("evidence target domains"));
    }
    total = total
      .checked_add(count)
      .ok_or(PersistedInviteAbuseError::CounterOverflow)?;
  }
  if total != requested {
    return Err(PersistedInviteAbuseError::Inconsistent("requested counter"));
  }
  Ok(())
}

pub fn validate_persisted_invite_abuse(
  record: &PersistedInviteAbuseRecord,
) -> Result<InviteAbuseSubject, PersistedInviteAbuseError> {
  let kind = decode_kind(&record.subject_kind)?;
  let status = decode_status(&record.subject_status)?;
  let action = decode_action(required(&record.subject_action, "subject action")?)?;
  let reason = decode_reason(required(&record.subject_reason, "subject reason")?)?;
  if required(&record.evidence_action, "evidence action")? != action.as_str()
    || record
      .action_action
      .as_deref()
      .is_some_and(|value| value != action.as_str())
  {
    return Err(PersistedInviteAbuseError::Inconsistent("action"));
  }
  if required(&record.evidence_reason, "evidence reason")? != reason.as_str() {
    return Err(PersistedInviteAbuseError::Inconsistent("reason"));
  }
  if required(&record.evidence_subject_key, "evidence subject key")? != record.subject_key {
    return Err(PersistedInviteAbuseError::Inconsistent("subject key"));
  }
  if invite_abuse_status(kind, action, reason) != Some(status) {
    return Err(PersistedInviteAbuseError::Invalid("subject combination"));
  }

  let evidence_user_id = required(&record.evidence_user_id, "evidence user id")?;
  let workspace_id = required(&record.evidence_workspace_id, "evidence workspace id")?;
  let actor_email_hash = required(&record.subject_actor_email_hash, "subject actor email hash")?;
  if evidence_user_id.is_empty() || workspace_id.is_empty() {
    return Err(PersistedInviteAbuseError::Invalid("evidence identity"));
  }
  if required(&record.evidence_actor_email_hash, "evidence actor email hash")? != actor_email_hash {
    return Err(PersistedInviteAbuseError::Inconsistent("actor email hash"));
  }
  if !hash_identity(actor_email_hash, "actor_email_sha256:v1:", 64) {
    return Err(PersistedInviteAbuseError::Invalid("actor email hash"));
  }
  let email_domain = required(&record.subject_email_domain, "subject email domain")?;
  if email_domain != normalize_domain(email_domain) || !email_domain.contains('.') {
    return Err(PersistedInviteAbuseError::Invalid("subject email domain"));
  }
  if let Some(source_prefix_hash) = &record.evidence_source_prefix_hash
    && !hash_identity(source_prefix_hash, "", 24)
  {
    return Err(PersistedInviteAbuseError::Invalid("evidence source prefix hash"));
  }
  if record
    .evidence_source_asn
    .is_some_and(|asn| u32::try_from(asn).is_err())
  {
    return Err(PersistedInviteAbuseError::Invalid("evidence source ASN"));
  }

  match kind {
    InviteAbuseSubjectKind::ActorEmail => {
      if record.subject_key != actor_email_hash || record.subject_user_id.as_deref() != Some(evidence_user_id) {
        return Err(PersistedInviteAbuseError::Invalid("actor subject identity"));
      }
    }
    InviteAbuseSubjectKind::Workspace => {
      if record.subject_user_id.is_some() || record.subject_key != workspace_subject_key(workspace_id) {
        return Err(PersistedInviteAbuseError::Invalid("workspace subject identity"));
      }
    }
    InviteAbuseSubjectKind::SourcePrefixDomain => {
      if record.subject_user_id.is_some()
        || !hash_identity(&record.subject_key, "source_prefix_domain_sha256:v1:", 24)
        || record.evidence_source_prefix_hash.is_none()
      {
        return Err(PersistedInviteAbuseError::Invalid("source cohort subject identity"));
      }
    }
  }

  let counters = record
    .evidence_counters
    .as_ref()
    .and_then(Value::as_object)
    .filter(|counters| counters.len() == 1)
    .ok_or(PersistedInviteAbuseError::Invalid("evidence counters"))?;
  let requested = counters
    .get("requested")
    .and_then(Value::as_i64)
    .and_then(|requested| i32::try_from(requested).ok())
    .filter(|requested| *requested > 0)
    .ok_or(PersistedInviteAbuseError::Invalid("evidence counters"))?;
  validate_targets(
    record
      .evidence_target_domains
      .as_ref()
      .ok_or(PersistedInviteAbuseError::Missing("evidence target domains"))?,
    requested,
  )?;

  Ok(InviteAbuseSubject {
    kind,
    key: record.subject_key.clone(),
    status,
  })
}