use super::Region;
use crate::ids::types::{AdequacyStatus, IdsError, IdsResult, IdsUri, Party};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GdprArticle {
Article45,
Article46,
Article47,
Article49,
}
impl GdprArticle {
pub fn description(&self) -> &'static str {
match self {
GdprArticle::Article45 => "Adequacy Decision",
GdprArticle::Article46 => "Appropriate Safeguards",
GdprArticle::Article47 => "Binding Corporate Rules",
GdprArticle::Article49 => "Derogations for Specific Situations",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Safeguard {
StandardContractualClauses {
decision_reference: String,
signed_date: DateTime<Utc>,
parties: Vec<Party>,
},
BindingCorporateRules {
approval_reference: String,
approving_authority: String,
approval_date: DateTime<Utc>,
},
CodeOfConduct {
code_name: String,
monitoring_body: String,
commitment_date: DateTime<Utc>,
},
Certification {
scheme_name: String,
certification_body: String,
certificate_id: String,
valid_until: DateTime<Utc>,
},
AdHocContractualClauses {
authorizing_authority: String,
authorization_reference: String,
},
AdministrativeArrangements {
public_body: String,
arrangement_reference: String,
},
}
impl Safeguard {
pub fn is_valid(&self, now: DateTime<Utc>) -> bool {
match self {
Safeguard::Certification { valid_until, .. } => *valid_until > now,
_ => true,
}
}
pub fn safeguard_type(&self) -> &'static str {
match self {
Safeguard::StandardContractualClauses { .. } => "Standard Contractual Clauses",
Safeguard::BindingCorporateRules { .. } => "Binding Corporate Rules",
Safeguard::CodeOfConduct { .. } => "Code of Conduct",
Safeguard::Certification { .. } => "Certification",
Safeguard::AdHocContractualClauses { .. } => "Ad-hoc Contractual Clauses",
Safeguard::AdministrativeArrangements { .. } => "Administrative Arrangements",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Article49Derogation {
ExplicitConsent {
data_subject: String,
consent_date: DateTime<Utc>,
informed_of_risks: bool,
},
ContractPerformance {
contract_reference: String,
},
ContractInInterest {
contract_reference: String,
interest_description: String,
},
PublicInterest {
legal_basis: String,
},
LegalClaims {
proceedings_type: String,
},
VitalInterests {
interest_description: String,
},
PublicRegister {
register_name: String,
},
CompellingLegitimateInterests {
interest_description: String,
not_repetitive: bool,
limited_data_subjects: bool,
safeguards_considered: bool,
},
}
impl Article49Derogation {
pub fn is_valid(&self) -> bool {
match self {
Article49Derogation::ExplicitConsent {
informed_of_risks, ..
} => *informed_of_risks,
Article49Derogation::CompellingLegitimateInterests {
not_repetitive,
limited_data_subjects,
safeguards_considered,
..
} => *not_repetitive && *limited_data_subjects && *safeguards_considered,
_ => true,
}
}
pub fn derogation_type(&self) -> &'static str {
match self {
Article49Derogation::ExplicitConsent { .. } => "Explicit Consent",
Article49Derogation::ContractPerformance { .. } => "Contract Performance",
Article49Derogation::ContractInInterest { .. } => "Contract in Interest",
Article49Derogation::PublicInterest { .. } => "Public Interest",
Article49Derogation::LegalClaims { .. } => "Legal Claims",
Article49Derogation::VitalInterests { .. } => "Vital Interests",
Article49Derogation::PublicRegister { .. } => "Public Register",
Article49Derogation::CompellingLegitimateInterests { .. } => {
"Compelling Legitimate Interests"
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferComplianceResult {
pub compliant: bool,
pub article: Option<GdprArticle>,
pub legal_basis_detail: String,
pub non_compliance_reasons: Vec<String>,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferRecord {
pub id: String,
pub timestamp: DateTime<Utc>,
pub from_region: String,
pub to_region: String,
pub data_categories: Vec<String>,
pub legal_basis: GdprArticle,
pub safeguard_detail: Option<String>,
pub controller: Party,
pub processor: Option<Party>,
pub purpose: String,
pub notes: Option<String>,
}
impl TransferRecord {
pub fn new(
from_region: impl Into<String>,
to_region: impl Into<String>,
legal_basis: GdprArticle,
controller: Party,
purpose: impl Into<String>,
) -> Self {
Self {
id: format!("transfer-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)),
timestamp: Utc::now(),
from_region: from_region.into(),
to_region: to_region.into(),
data_categories: Vec::new(),
legal_basis,
safeguard_detail: None,
controller,
processor: None,
purpose: purpose.into(),
notes: None,
}
}
pub fn with_data_category(mut self, category: impl Into<String>) -> Self {
self.data_categories.push(category.into());
self
}
pub fn with_data_categories(mut self, categories: Vec<String>) -> Self {
self.data_categories.extend(categories);
self
}
pub fn with_safeguard_detail(mut self, detail: impl Into<String>) -> Self {
self.safeguard_detail = Some(detail.into());
self
}
pub fn with_processor(mut self, processor: Party) -> Self {
self.processor = Some(processor);
self
}
pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
self.notes = Some(notes.into());
self
}
}
type SafeguardsMap = HashMap<(String, String), Vec<Safeguard>>;
pub struct GdprComplianceChecker {
safeguards: Arc<RwLock<SafeguardsMap>>,
bcrs: Arc<RwLock<HashMap<String, Safeguard>>>,
transfer_log: Arc<RwLock<Vec<TransferRecord>>>,
}
impl Default for GdprComplianceChecker {
fn default() -> Self {
Self::new()
}
}
impl GdprComplianceChecker {
pub fn new() -> Self {
Self {
safeguards: Arc::new(RwLock::new(HashMap::new())),
bcrs: Arc::new(RwLock::new(HashMap::new())),
transfer_log: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn register_safeguard(
&self,
from_org: impl Into<String>,
to_org: impl Into<String>,
safeguard: Safeguard,
) {
let mut safeguards = self.safeguards.write().await;
let key = (from_org.into(), to_org.into());
safeguards.entry(key).or_default().push(safeguard);
}
pub async fn register_bcr(&self, organization: impl Into<String>, bcr: Safeguard) {
let mut bcrs = self.bcrs.write().await;
bcrs.insert(organization.into(), bcr);
}
pub async fn check_transfer_compliance(
&self,
from: &Region,
to: &Region,
from_org: Option<&str>,
to_org: Option<&str>,
) -> IdsResult<TransferComplianceResult> {
let now = Utc::now();
let mut result = TransferComplianceResult {
compliant: false,
article: None,
legal_basis_detail: String::new(),
non_compliance_reasons: Vec::new(),
recommendations: Vec::new(),
};
if self.is_eea_internal_transfer(from, to) {
result.compliant = true;
result.legal_basis_detail =
"Internal EEA transfer - no Chapter V restrictions apply".to_string();
return Ok(result);
}
if to.adequacy == AdequacyStatus::Adequate {
result.compliant = true;
result.article = Some(GdprArticle::Article45);
result.legal_basis_detail = format!("Adequacy decision in effect for {}", to.name);
return Ok(result);
}
if let Some(safeguard) = self.find_valid_safeguard(from_org, to_org, now).await {
let article = if matches!(safeguard, Safeguard::BindingCorporateRules { .. }) {
GdprArticle::Article47
} else {
GdprArticle::Article46
};
result.compliant = true;
result.article = Some(article);
result.legal_basis_detail = format!(
"{} in place: {}",
safeguard.safeguard_type(),
self.safeguard_summary(&safeguard)
);
return Ok(result);
}
result
.non_compliance_reasons
.push(format!("No adequacy decision for {}", to.name));
result
.non_compliance_reasons
.push("No appropriate safeguards (SCCs, BCRs, etc.) registered".to_string());
result
.recommendations
.push("Consider implementing Standard Contractual Clauses (Art. 46(2)(c))".to_string());
result.recommendations.push(
"Verify if Binding Corporate Rules apply within your corporate group".to_string(),
);
result
.recommendations
.push("Evaluate if Article 49 derogations apply for specific situations".to_string());
Ok(result)
}
pub fn check_derogation(&self, derogation: &Article49Derogation) -> TransferComplianceResult {
let mut result = TransferComplianceResult {
compliant: false,
article: Some(GdprArticle::Article49),
legal_basis_detail: String::new(),
non_compliance_reasons: Vec::new(),
recommendations: Vec::new(),
};
if derogation.is_valid() {
result.compliant = true;
result.legal_basis_detail =
format!("Article 49 derogation: {}", derogation.derogation_type());
} else {
result.non_compliance_reasons.push(format!(
"Article 49 derogation '{}' does not meet all requirements",
derogation.derogation_type()
));
match derogation {
Article49Derogation::ExplicitConsent {
informed_of_risks, ..
} if !informed_of_risks => {
result.recommendations.push(
"Data subject must be informed of risks before giving consent".to_string(),
);
}
Article49Derogation::CompellingLegitimateInterests {
not_repetitive,
limited_data_subjects,
safeguards_considered,
..
} => {
if !not_repetitive {
result
.recommendations
.push("Transfer must not be repetitive".to_string());
}
if !limited_data_subjects {
result.recommendations.push(
"Transfer must concern only a limited number of data subjects"
.to_string(),
);
}
if !safeguards_considered {
result.recommendations.push(
"Appropriate safeguards must be assessed and documented".to_string(),
);
}
}
_ => {}
}
}
result
}
pub async fn record_transfer(&self, record: TransferRecord) -> IdsResult<()> {
let mut log = self.transfer_log.write().await;
log.push(record);
Ok(())
}
pub async fn get_transfer_records(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Vec<TransferRecord> {
let log = self.transfer_log.read().await;
log.iter()
.filter(|r| r.timestamp >= from && r.timestamp <= to)
.cloned()
.collect()
}
pub async fn get_transfers_to_region(&self, region_code: &str) -> Vec<TransferRecord> {
let log = self.transfer_log.read().await;
log.iter()
.filter(|r| r.to_region == region_code)
.cloned()
.collect()
}
fn is_eea_internal_transfer(&self, from: &Region, to: &Region) -> bool {
let eea_countries = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE",
"IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
"IS", "LI", "NO",
];
let from_eea = eea_countries.contains(&from.jurisdiction.country_code.as_str());
let to_eea = eea_countries.contains(&to.jurisdiction.country_code.as_str());
from_eea && to_eea
}
async fn find_valid_safeguard(
&self,
from_org: Option<&str>,
to_org: Option<&str>,
now: DateTime<Utc>,
) -> Option<Safeguard> {
if let Some(to) = to_org {
let bcrs = self.bcrs.read().await;
if let Some(bcr) = bcrs.get(to) {
if bcr.is_valid(now) {
return Some(bcr.clone());
}
}
}
if let (Some(from), Some(to)) = (from_org, to_org) {
let safeguards = self.safeguards.read().await;
let key = (from.to_string(), to.to_string());
if let Some(org_safeguards) = safeguards.get(&key) {
for safeguard in org_safeguards {
if safeguard.is_valid(now) {
return Some(safeguard.clone());
}
}
}
}
None
}
fn safeguard_summary(&self, safeguard: &Safeguard) -> String {
match safeguard {
Safeguard::StandardContractualClauses {
decision_reference,
signed_date,
..
} => {
format!(
"SCCs ref. {} (signed {})",
decision_reference,
signed_date.format("%Y-%m-%d")
)
}
Safeguard::BindingCorporateRules {
approval_reference,
approving_authority,
..
} => {
format!(
"BCR ref. {} (approved by {})",
approval_reference, approving_authority
)
}
Safeguard::CodeOfConduct {
code_name,
monitoring_body,
..
} => {
format!("CoC '{}' (monitored by {})", code_name, monitoring_body)
}
Safeguard::Certification {
certificate_id,
certification_body,
valid_until,
..
} => {
format!(
"Cert. {} by {} (valid until {})",
certificate_id,
certification_body,
valid_until.format("%Y-%m-%d")
)
}
Safeguard::AdHocContractualClauses {
authorization_reference,
..
} => {
format!("Ad-hoc clauses ref. {}", authorization_reference)
}
Safeguard::AdministrativeArrangements {
arrangement_reference,
..
} => {
format!("Admin. arrangement ref. {}", arrangement_reference)
}
}
}
pub fn check_transfer_compliance_simple(from: &Region, to: &Region) -> IdsResult<GdprArticle> {
if to.adequacy == AdequacyStatus::Adequate {
return Ok(GdprArticle::Article45);
}
if from.jurisdiction.country_code == "EU"
|| Self::is_eu_member(&from.jurisdiction.country_code)
{
return Ok(GdprArticle::Article46);
}
Err(IdsError::GdprViolation(format!(
"No legal basis for transfer from {} to {}",
from.name, to.name
)))
}
fn is_eu_member(code: &str) -> bool {
let eu_members = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE",
"IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
];
eu_members.contains(&code)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::types::IdsUri;
fn test_party() -> Party {
Party {
id: IdsUri::new("https://example.org/party/1").expect("valid uri"),
name: "Test Party".to_string(),
legal_name: None,
description: None,
contact: None,
gaiax_participant_id: None,
}
}
#[test]
fn test_gdpr_adequacy_transfer() {
let eu = Region::eu_member("DE", "Germany");
let jp = Region::japan();
let result = GdprComplianceChecker::check_transfer_compliance_simple(&eu, &jp);
assert!(result.is_ok());
assert_eq!(result.expect("should be ok"), GdprArticle::Article45);
}
#[test]
fn test_gdpr_article_description() {
assert_eq!(GdprArticle::Article45.description(), "Adequacy Decision");
assert_eq!(
GdprArticle::Article46.description(),
"Appropriate Safeguards"
);
assert_eq!(
GdprArticle::Article47.description(),
"Binding Corporate Rules"
);
assert_eq!(
GdprArticle::Article49.description(),
"Derogations for Specific Situations"
);
}
#[tokio::test]
async fn test_eea_internal_transfer() {
let checker = GdprComplianceChecker::new();
let germany = Region::eu_member("DE", "Germany");
let france = Region::eu_member("FR", "France");
let result = checker
.check_transfer_compliance(&germany, &france, None, None)
.await
.expect("should succeed");
assert!(result.compliant);
assert!(result.legal_basis_detail.contains("Internal EEA"));
}
#[tokio::test]
async fn test_sccs_safeguard() {
let checker = GdprComplianceChecker::new();
let sccs = Safeguard::StandardContractualClauses {
decision_reference: "2021/914".to_string(),
signed_date: Utc::now(),
parties: vec![test_party()],
};
checker.register_safeguard("org-eu", "org-us", sccs).await;
let safeguard = checker
.find_valid_safeguard(Some("org-eu"), Some("org-us"), Utc::now())
.await;
assert!(safeguard.is_some());
assert_eq!(
safeguard.expect("should exist").safeguard_type(),
"Standard Contractual Clauses"
);
}
#[tokio::test]
async fn test_bcr_registration() {
let checker = GdprComplianceChecker::new();
let bcr = Safeguard::BindingCorporateRules {
approval_reference: "BCR-2023-001".to_string(),
approving_authority: "German DPA".to_string(),
approval_date: Utc::now(),
};
checker.register_bcr("mega-corp", bcr).await;
let safeguard = checker
.find_valid_safeguard(Some("any"), Some("mega-corp"), Utc::now())
.await;
assert!(safeguard.is_some());
assert_eq!(
safeguard.expect("should exist").safeguard_type(),
"Binding Corporate Rules"
);
}
#[test]
fn test_explicit_consent_derogation() {
let checker = GdprComplianceChecker::new();
let consent = Article49Derogation::ExplicitConsent {
data_subject: "user@example.com".to_string(),
consent_date: Utc::now(),
informed_of_risks: true,
};
let result = checker.check_derogation(&consent);
assert!(result.compliant);
assert_eq!(result.article, Some(GdprArticle::Article49));
}
#[test]
fn test_invalid_consent_derogation() {
let checker = GdprComplianceChecker::new();
let consent = Article49Derogation::ExplicitConsent {
data_subject: "user@example.com".to_string(),
consent_date: Utc::now(),
informed_of_risks: false, };
let result = checker.check_derogation(&consent);
assert!(!result.compliant);
assert!(!result.recommendations.is_empty());
}
#[test]
fn test_compelling_legitimate_interests() {
let checker = GdprComplianceChecker::new();
let derogation = Article49Derogation::CompellingLegitimateInterests {
interest_description: "Emergency data recovery".to_string(),
not_repetitive: true,
limited_data_subjects: true,
safeguards_considered: true,
};
let result = checker.check_derogation(&derogation);
assert!(result.compliant);
}
#[tokio::test]
async fn test_transfer_record() {
let checker = GdprComplianceChecker::new();
let record = TransferRecord::new(
"DE",
"US",
GdprArticle::Article46,
test_party(),
"Data analytics",
)
.with_data_category("Personal data")
.with_safeguard_detail("SCCs 2021/914");
checker
.record_transfer(record)
.await
.expect("should succeed");
let records = checker.get_transfers_to_region("US").await;
assert_eq!(records.len(), 1);
assert_eq!(records[0].purpose, "Data analytics");
}
#[test]
fn test_certification_validity() {
let valid_cert = Safeguard::Certification {
scheme_name: "EU-US DPF".to_string(),
certification_body: "Cert Body".to_string(),
certificate_id: "CERT-001".to_string(),
valid_until: Utc::now() + chrono::Duration::days(365),
};
let expired_cert = Safeguard::Certification {
scheme_name: "EU-US DPF".to_string(),
certification_body: "Cert Body".to_string(),
certificate_id: "CERT-002".to_string(),
valid_until: Utc::now() - chrono::Duration::days(1),
};
assert!(valid_cert.is_valid(Utc::now()));
assert!(!expired_cert.is_valid(Utc::now()));
}
}