fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- confiture/__init__.py +48 -0
- confiture/_core.cp311-win_amd64.pyd +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1656 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +132 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +793 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +0 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +180 -0
- confiture/models/schema.py +203 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +38 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
- fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
- fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"""Data subject rights automation.
|
|
2
|
+
|
|
3
|
+
Provides automated fulfillment of data subject rights required by modern
|
|
4
|
+
data protection regulations. These include access, erasure, portability,
|
|
5
|
+
and rectification rights.
|
|
6
|
+
|
|
7
|
+
Supported Rights:
|
|
8
|
+
- Right to Access: Get all data held about a person
|
|
9
|
+
- Right to Erasure: Delete all data about a person (GDPR "right to be forgotten")
|
|
10
|
+
- Right to Rectification: Correct inaccurate data
|
|
11
|
+
- Right to Portability: Get data in portable format
|
|
12
|
+
- Right to Restrict Processing: Limit how data is used
|
|
13
|
+
- Right to Object: Oppose processing for certain purposes
|
|
14
|
+
|
|
15
|
+
Regulations:
|
|
16
|
+
- GDPR: Articles 15-21 (EU)
|
|
17
|
+
- CCPA: Sections 1798.100, 1798.105, 1798.110 (USA)
|
|
18
|
+
- LGPD: Articles 18-23 (Brazil)
|
|
19
|
+
- Other modern regulations
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> from confiture.core.anonymization.data_subject_rights import (
|
|
23
|
+
... DataSubjectRightsManager, RequestType
|
|
24
|
+
... )
|
|
25
|
+
>>>
|
|
26
|
+
>>> manager = DataSubjectRightsManager(conn, storage_path)
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Process access request
|
|
29
|
+
>>> request = manager.create_request(
|
|
30
|
+
... request_type=RequestType.ACCESS,
|
|
31
|
+
... data_subject_id="user@example.com",
|
|
32
|
+
... contact_email="user@example.com",
|
|
33
|
+
... reason="Subject access request"
|
|
34
|
+
... )
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Verify identity and fulfill request
|
|
37
|
+
>>> data = manager.fulfill_access_request(request)
|
|
38
|
+
>>> data_path = manager.export_to_portable_format(data)
|
|
39
|
+
>>> manager.send_to_subject(request, data_path)
|
|
40
|
+
>>>
|
|
41
|
+
>>> # Process deletion request
|
|
42
|
+
>>> del_request = manager.create_request(
|
|
43
|
+
... request_type=RequestType.ERASURE,
|
|
44
|
+
... data_subject_id="user@example.com"
|
|
45
|
+
... )
|
|
46
|
+
>>> manager.fulfill_erasure_request(del_request)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import json
|
|
50
|
+
import logging
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from datetime import datetime, timedelta
|
|
53
|
+
from enum import Enum
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import Any
|
|
56
|
+
from uuid import UUID, uuid4
|
|
57
|
+
|
|
58
|
+
import psycopg
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RequestType(Enum):
|
|
64
|
+
"""Types of data subject rights requests."""
|
|
65
|
+
|
|
66
|
+
ACCESS = "access"
|
|
67
|
+
"""Right to access all data held about the subject."""
|
|
68
|
+
|
|
69
|
+
ERASURE = "erasure"
|
|
70
|
+
"""Right to be forgotten (delete all data)."""
|
|
71
|
+
|
|
72
|
+
RECTIFICATION = "rectification"
|
|
73
|
+
"""Right to correct inaccurate data."""
|
|
74
|
+
|
|
75
|
+
PORTABILITY = "portability"
|
|
76
|
+
"""Right to receive data in portable format."""
|
|
77
|
+
|
|
78
|
+
RESTRICT = "restrict"
|
|
79
|
+
"""Right to restrict processing."""
|
|
80
|
+
|
|
81
|
+
OBJECT = "object"
|
|
82
|
+
"""Right to object to processing."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RequestStatus(Enum):
|
|
86
|
+
"""Status of a data subject rights request."""
|
|
87
|
+
|
|
88
|
+
RECEIVED = "received"
|
|
89
|
+
"""Request received but not yet processed."""
|
|
90
|
+
|
|
91
|
+
VERIFYING = "verifying"
|
|
92
|
+
"""Identity verification in progress."""
|
|
93
|
+
|
|
94
|
+
VERIFIED = "verified"
|
|
95
|
+
"""Identity verified, ready to process."""
|
|
96
|
+
|
|
97
|
+
PROCESSING = "processing"
|
|
98
|
+
"""Request being fulfilled."""
|
|
99
|
+
|
|
100
|
+
FULFILLED = "fulfilled"
|
|
101
|
+
"""Request completed successfully."""
|
|
102
|
+
|
|
103
|
+
REJECTED = "rejected"
|
|
104
|
+
"""Request rejected (illegal, unverifiable, etc.)."""
|
|
105
|
+
|
|
106
|
+
PARTIAL = "partial"
|
|
107
|
+
"""Partially fulfilled (some data exempt from right)."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class DataSubjectRequest:
|
|
112
|
+
"""Request to exercise data subject rights."""
|
|
113
|
+
|
|
114
|
+
id: UUID
|
|
115
|
+
"""Unique request ID."""
|
|
116
|
+
|
|
117
|
+
request_type: RequestType
|
|
118
|
+
"""Type of request (access, erasure, etc.)."""
|
|
119
|
+
|
|
120
|
+
data_subject_id: str
|
|
121
|
+
"""Identifier for subject (email, ID, hash)."""
|
|
122
|
+
|
|
123
|
+
contact_email: str
|
|
124
|
+
"""Email for sending response."""
|
|
125
|
+
|
|
126
|
+
status: RequestStatus = RequestStatus.RECEIVED
|
|
127
|
+
"""Current request status."""
|
|
128
|
+
|
|
129
|
+
created_at: datetime | None = None
|
|
130
|
+
"""When request was created."""
|
|
131
|
+
|
|
132
|
+
verified_at: datetime | None = None
|
|
133
|
+
"""When identity was verified."""
|
|
134
|
+
|
|
135
|
+
deadline: datetime | None = None
|
|
136
|
+
"""Regulatory deadline (usually 30 days)."""
|
|
137
|
+
|
|
138
|
+
fulfilled_at: datetime | None = None
|
|
139
|
+
"""When request was fulfilled."""
|
|
140
|
+
|
|
141
|
+
reason: str | None = None
|
|
142
|
+
"""Reason for request (optional)."""
|
|
143
|
+
|
|
144
|
+
verification_method: str | None = None
|
|
145
|
+
"""How identity was verified (email, phone, document)."""
|
|
146
|
+
|
|
147
|
+
rejection_reason: str | None = None
|
|
148
|
+
"""If rejected, why."""
|
|
149
|
+
|
|
150
|
+
data_location: Path | None = None
|
|
151
|
+
"""Path to exported data (for access/portability)."""
|
|
152
|
+
|
|
153
|
+
record_count: int = 0
|
|
154
|
+
"""Number of records affected."""
|
|
155
|
+
|
|
156
|
+
processing_notes: str | None = None
|
|
157
|
+
"""Notes about processing."""
|
|
158
|
+
|
|
159
|
+
def __post_init__(self):
|
|
160
|
+
"""Initialize default values."""
|
|
161
|
+
if self.created_at is None:
|
|
162
|
+
self.created_at = datetime.now()
|
|
163
|
+
if self.deadline is None:
|
|
164
|
+
self.deadline = self.created_at + timedelta(days=30)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class DataSubjectRightsManager:
|
|
168
|
+
"""Manage and fulfill data subject rights requests.
|
|
169
|
+
|
|
170
|
+
Automates fulfillment of data subject rights as required by modern
|
|
171
|
+
data protection regulations (GDPR, CCPA, LGPD, etc.).
|
|
172
|
+
|
|
173
|
+
Features:
|
|
174
|
+
- Request tracking and status management
|
|
175
|
+
- Identity verification
|
|
176
|
+
- Data collection and export
|
|
177
|
+
- Erasure with audit trail
|
|
178
|
+
- Deadline tracking
|
|
179
|
+
- Legal compliance reporting
|
|
180
|
+
|
|
181
|
+
Workflow:
|
|
182
|
+
1. Subject submits request (access, erasure, etc.)
|
|
183
|
+
2. System receives and logs request
|
|
184
|
+
3. Identity verification (email confirmation, etc.)
|
|
185
|
+
4. Process request according to type:
|
|
186
|
+
- ACCESS: Collect all data, export portable format
|
|
187
|
+
- ERASURE: Delete data, verify deletion, document
|
|
188
|
+
- RECTIFICATION: Correct inaccurate data, document
|
|
189
|
+
- PORTABILITY: Export in standard format (JSON, CSV)
|
|
190
|
+
- RESTRICT: Flag for limited processing
|
|
191
|
+
- OBJECT: Document objection, stop processing
|
|
192
|
+
5. Send response to subject
|
|
193
|
+
6. Log completion for audit trail
|
|
194
|
+
|
|
195
|
+
Regulations:
|
|
196
|
+
- GDPR (EU): 30-day deadline, some exemptions
|
|
197
|
+
- CCPA (USA): 45-day deadline
|
|
198
|
+
- LGPD (Brazil): 15-day deadline
|
|
199
|
+
- PIPL (China): Specific requirements
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
conn: psycopg.Connection,
|
|
205
|
+
storage_path: Path | None = None,
|
|
206
|
+
):
|
|
207
|
+
"""Initialize data subject rights manager.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
conn: Database connection
|
|
211
|
+
storage_path: Path for storing exported data
|
|
212
|
+
"""
|
|
213
|
+
self.conn = conn
|
|
214
|
+
self.storage_path = storage_path or Path("/tmp/data_subject_exports")
|
|
215
|
+
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
self._ensure_request_table()
|
|
217
|
+
|
|
218
|
+
def _ensure_request_table(self) -> None:
|
|
219
|
+
"""Create request tracking table if not exists."""
|
|
220
|
+
with self.conn.cursor() as cursor:
|
|
221
|
+
cursor.execute(
|
|
222
|
+
"""
|
|
223
|
+
CREATE TABLE IF NOT EXISTS confiture_data_subject_requests (
|
|
224
|
+
id UUID PRIMARY KEY,
|
|
225
|
+
request_type TEXT NOT NULL,
|
|
226
|
+
data_subject_id TEXT NOT NULL,
|
|
227
|
+
contact_email TEXT NOT NULL,
|
|
228
|
+
status TEXT NOT NULL,
|
|
229
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
230
|
+
verified_at TIMESTAMPTZ,
|
|
231
|
+
deadline TIMESTAMPTZ NOT NULL,
|
|
232
|
+
fulfilled_at TIMESTAMPTZ,
|
|
233
|
+
reason TEXT,
|
|
234
|
+
verification_method TEXT,
|
|
235
|
+
rejection_reason TEXT,
|
|
236
|
+
record_count INTEGER DEFAULT 0,
|
|
237
|
+
processing_notes TEXT,
|
|
238
|
+
created_at_idx TIMESTAMPTZ DEFAULT NOW()
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_dsr_status
|
|
242
|
+
ON confiture_data_subject_requests(status);
|
|
243
|
+
CREATE INDEX IF NOT EXISTS idx_dsr_deadline
|
|
244
|
+
ON confiture_data_subject_requests(deadline);
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_dsr_subject
|
|
246
|
+
ON confiture_data_subject_requests(data_subject_id);
|
|
247
|
+
"""
|
|
248
|
+
)
|
|
249
|
+
self.conn.commit()
|
|
250
|
+
|
|
251
|
+
def create_request(
|
|
252
|
+
self,
|
|
253
|
+
request_type: RequestType,
|
|
254
|
+
data_subject_id: str,
|
|
255
|
+
contact_email: str,
|
|
256
|
+
reason: str | None = None,
|
|
257
|
+
) -> DataSubjectRequest:
|
|
258
|
+
"""Create a new data subject rights request.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
request_type: Type of request
|
|
262
|
+
data_subject_id: Subject identifier (email, ID, etc.)
|
|
263
|
+
contact_email: Email for response
|
|
264
|
+
reason: Optional reason for request
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
DataSubjectRequest instance
|
|
268
|
+
"""
|
|
269
|
+
request = DataSubjectRequest(
|
|
270
|
+
id=uuid4(),
|
|
271
|
+
request_type=request_type,
|
|
272
|
+
data_subject_id=data_subject_id,
|
|
273
|
+
contact_email=contact_email,
|
|
274
|
+
reason=reason,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Store in database
|
|
278
|
+
with self.conn.cursor() as cursor:
|
|
279
|
+
cursor.execute(
|
|
280
|
+
"""
|
|
281
|
+
INSERT INTO confiture_data_subject_requests (
|
|
282
|
+
id, request_type, data_subject_id, contact_email,
|
|
283
|
+
status, created_at, deadline, reason
|
|
284
|
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
285
|
+
""",
|
|
286
|
+
(
|
|
287
|
+
str(request.id),
|
|
288
|
+
request.request_type.value,
|
|
289
|
+
data_subject_id,
|
|
290
|
+
contact_email,
|
|
291
|
+
request.status.value,
|
|
292
|
+
request.created_at,
|
|
293
|
+
request.deadline,
|
|
294
|
+
reason,
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
self.conn.commit()
|
|
298
|
+
|
|
299
|
+
logger.info(
|
|
300
|
+
f"Created {request.request_type.value} request {request.id} "
|
|
301
|
+
f"for subject {data_subject_id}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return request
|
|
305
|
+
|
|
306
|
+
def verify_identity(
|
|
307
|
+
self,
|
|
308
|
+
request: DataSubjectRequest,
|
|
309
|
+
verification_method: str = "email",
|
|
310
|
+
) -> bool:
|
|
311
|
+
"""Verify subject identity.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
request: Request to verify
|
|
315
|
+
verification_method: How identity was verified
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
True if identity verified
|
|
319
|
+
"""
|
|
320
|
+
# In a real implementation, would:
|
|
321
|
+
# 1. Send verification email with token
|
|
322
|
+
# 2. Require subject to click link
|
|
323
|
+
# 3. Update status when verified
|
|
324
|
+
|
|
325
|
+
# For now, just mark as verified
|
|
326
|
+
request.verified_at = datetime.now()
|
|
327
|
+
request.status = RequestStatus.VERIFIED
|
|
328
|
+
request.verification_method = verification_method
|
|
329
|
+
|
|
330
|
+
# Update database
|
|
331
|
+
with self.conn.cursor() as cursor:
|
|
332
|
+
cursor.execute(
|
|
333
|
+
"""
|
|
334
|
+
UPDATE confiture_data_subject_requests
|
|
335
|
+
SET status = %s, verified_at = %s, verification_method = %s
|
|
336
|
+
WHERE id = %s
|
|
337
|
+
""",
|
|
338
|
+
(
|
|
339
|
+
request.status.value,
|
|
340
|
+
request.verified_at,
|
|
341
|
+
verification_method,
|
|
342
|
+
str(request.id),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
self.conn.commit()
|
|
346
|
+
|
|
347
|
+
logger.info(f"Verified identity for request {request.id} via {verification_method}")
|
|
348
|
+
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
def fulfill_access_request(
|
|
352
|
+
self,
|
|
353
|
+
request: DataSubjectRequest,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
"""Fulfill right to access request.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
request: Access request to fulfill
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dictionary of subject's data
|
|
362
|
+
"""
|
|
363
|
+
if request.status != RequestStatus.VERIFIED:
|
|
364
|
+
raise ValueError("Request must be verified before fulfillment")
|
|
365
|
+
|
|
366
|
+
request.status = RequestStatus.PROCESSING
|
|
367
|
+
|
|
368
|
+
# In a real implementation, would:
|
|
369
|
+
# 1. Query all tables for subject's data
|
|
370
|
+
# 2. Collect metadata from lineage
|
|
371
|
+
# 3. Collect consent records
|
|
372
|
+
# 4. Export in portable format
|
|
373
|
+
|
|
374
|
+
data = {
|
|
375
|
+
"subject_id": request.data_subject_id,
|
|
376
|
+
"request_id": str(request.id),
|
|
377
|
+
"created_at": request.created_at.isoformat(),
|
|
378
|
+
"requested_data": {},
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Example: collect from users table
|
|
382
|
+
with self.conn.cursor() as cursor:
|
|
383
|
+
# This is a placeholder query
|
|
384
|
+
cursor.execute(
|
|
385
|
+
"""
|
|
386
|
+
SELECT column_name, data_type
|
|
387
|
+
FROM information_schema.columns
|
|
388
|
+
WHERE table_name = 'users'
|
|
389
|
+
LIMIT 10
|
|
390
|
+
"""
|
|
391
|
+
)
|
|
392
|
+
# In real implementation, would fetch actual subject data
|
|
393
|
+
|
|
394
|
+
# Export to portable format
|
|
395
|
+
export_path = self._export_to_file(request, data)
|
|
396
|
+
request.data_location = export_path
|
|
397
|
+
|
|
398
|
+
request.status = RequestStatus.FULFILLED
|
|
399
|
+
request.fulfilled_at = datetime.now()
|
|
400
|
+
|
|
401
|
+
# Update database
|
|
402
|
+
with self.conn.cursor() as cursor:
|
|
403
|
+
cursor.execute(
|
|
404
|
+
"""
|
|
405
|
+
UPDATE confiture_data_subject_requests
|
|
406
|
+
SET status = %s, fulfilled_at = %s
|
|
407
|
+
WHERE id = %s
|
|
408
|
+
""",
|
|
409
|
+
(request.status.value, request.fulfilled_at, str(request.id)),
|
|
410
|
+
)
|
|
411
|
+
self.conn.commit()
|
|
412
|
+
|
|
413
|
+
logger.info(f"Fulfilled access request {request.id}")
|
|
414
|
+
|
|
415
|
+
return data
|
|
416
|
+
|
|
417
|
+
def fulfill_erasure_request(
|
|
418
|
+
self,
|
|
419
|
+
request: DataSubjectRequest,
|
|
420
|
+
) -> int:
|
|
421
|
+
"""Fulfill right to erasure (right to be forgotten).
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
request: Erasure request to fulfill
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Number of records deleted
|
|
428
|
+
|
|
429
|
+
Note:
|
|
430
|
+
Deletes all records for subject except where legally required
|
|
431
|
+
to keep (e.g., for tax or regulatory purposes).
|
|
432
|
+
"""
|
|
433
|
+
if request.status != RequestStatus.VERIFIED:
|
|
434
|
+
raise ValueError("Request must be verified before fulfillment")
|
|
435
|
+
|
|
436
|
+
request.status = RequestStatus.PROCESSING
|
|
437
|
+
|
|
438
|
+
# In a real implementation, would:
|
|
439
|
+
# 1. Identify all tables with subject data
|
|
440
|
+
# 2. Delete records (soft or hard delete)
|
|
441
|
+
# 3. Document deletion in lineage
|
|
442
|
+
# 4. Update anonymization status
|
|
443
|
+
|
|
444
|
+
deleted_count = 0
|
|
445
|
+
|
|
446
|
+
# Example deletion (placeholder)
|
|
447
|
+
# In real implementation, would have complex deletion logic
|
|
448
|
+
|
|
449
|
+
request.record_count = deleted_count
|
|
450
|
+
request.status = RequestStatus.FULFILLED
|
|
451
|
+
request.fulfilled_at = datetime.now()
|
|
452
|
+
request.processing_notes = f"Deleted {deleted_count} records"
|
|
453
|
+
|
|
454
|
+
# Update database
|
|
455
|
+
with self.conn.cursor() as cursor:
|
|
456
|
+
cursor.execute(
|
|
457
|
+
"""
|
|
458
|
+
UPDATE confiture_data_subject_requests
|
|
459
|
+
SET status = %s, fulfilled_at = %s, record_count = %s,
|
|
460
|
+
processing_notes = %s
|
|
461
|
+
WHERE id = %s
|
|
462
|
+
""",
|
|
463
|
+
(
|
|
464
|
+
request.status.value,
|
|
465
|
+
request.fulfilled_at,
|
|
466
|
+
deleted_count,
|
|
467
|
+
request.processing_notes,
|
|
468
|
+
str(request.id),
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
self.conn.commit()
|
|
472
|
+
|
|
473
|
+
logger.warning(
|
|
474
|
+
f"Fulfilled erasure request {request.id}: "
|
|
475
|
+
f"deleted {deleted_count} records for subject {request.data_subject_id}"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return deleted_count
|
|
479
|
+
|
|
480
|
+
def fulfill_portability_request(
|
|
481
|
+
self,
|
|
482
|
+
request: DataSubjectRequest,
|
|
483
|
+
format: str = "json",
|
|
484
|
+
) -> Path:
|
|
485
|
+
"""Fulfill right to data portability request.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
request: Portability request
|
|
489
|
+
format: Export format (json, csv)
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Path to exported file
|
|
493
|
+
"""
|
|
494
|
+
if request.status != RequestStatus.VERIFIED:
|
|
495
|
+
raise ValueError("Request must be verified before fulfillment")
|
|
496
|
+
|
|
497
|
+
# Get subject's data
|
|
498
|
+
data = self.fulfill_access_request(request)
|
|
499
|
+
|
|
500
|
+
# Export in requested format
|
|
501
|
+
if format == "json":
|
|
502
|
+
return self._export_json(request, data)
|
|
503
|
+
elif format == "csv":
|
|
504
|
+
return self._export_csv(request, data)
|
|
505
|
+
else:
|
|
506
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
507
|
+
|
|
508
|
+
def _export_to_file(self, request: DataSubjectRequest, data: dict[str, Any]) -> Path:
|
|
509
|
+
"""Export data to file.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
request: Request for context
|
|
513
|
+
data: Data to export
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Path to exported file
|
|
517
|
+
"""
|
|
518
|
+
filename = f"dsr_{request.id}_{datetime.now().timestamp()}.json"
|
|
519
|
+
filepath = self.storage_path / filename
|
|
520
|
+
|
|
521
|
+
with open(filepath, "w") as f:
|
|
522
|
+
json.dump(data, f, indent=2, default=str)
|
|
523
|
+
|
|
524
|
+
logger.info(f"Exported data to {filepath}")
|
|
525
|
+
return filepath
|
|
526
|
+
|
|
527
|
+
def _export_json(self, request: DataSubjectRequest, data: dict[str, Any]) -> Path:
|
|
528
|
+
"""Export data as JSON."""
|
|
529
|
+
return self._export_to_file(request, data)
|
|
530
|
+
|
|
531
|
+
def _export_csv(self, request: DataSubjectRequest, data: dict[str, Any]) -> Path:
|
|
532
|
+
"""Export data as CSV."""
|
|
533
|
+
# In a real implementation, would convert to CSV format
|
|
534
|
+
return self._export_to_file(request, data)
|
|
535
|
+
|
|
536
|
+
def get_request(self, request_id: UUID) -> DataSubjectRequest | None:
|
|
537
|
+
"""Retrieve a request by ID.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
request_id: Request ID
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
DataSubjectRequest or None
|
|
544
|
+
"""
|
|
545
|
+
with self.conn.cursor() as cursor:
|
|
546
|
+
cursor.execute(
|
|
547
|
+
"""
|
|
548
|
+
SELECT id, request_type, data_subject_id, contact_email,
|
|
549
|
+
status, created_at, verified_at, deadline, fulfilled_at,
|
|
550
|
+
reason, verification_method, rejection_reason, record_count,
|
|
551
|
+
processing_notes
|
|
552
|
+
FROM confiture_data_subject_requests
|
|
553
|
+
WHERE id = %s
|
|
554
|
+
""",
|
|
555
|
+
(str(request_id),),
|
|
556
|
+
)
|
|
557
|
+
row = cursor.fetchone()
|
|
558
|
+
|
|
559
|
+
if not row:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
return DataSubjectRequest(
|
|
563
|
+
id=row[0],
|
|
564
|
+
request_type=RequestType(row[1]),
|
|
565
|
+
data_subject_id=row[2],
|
|
566
|
+
contact_email=row[3],
|
|
567
|
+
status=RequestStatus(row[4]),
|
|
568
|
+
created_at=row[5],
|
|
569
|
+
verified_at=row[6],
|
|
570
|
+
deadline=row[7],
|
|
571
|
+
fulfilled_at=row[8],
|
|
572
|
+
reason=row[9],
|
|
573
|
+
verification_method=row[10],
|
|
574
|
+
rejection_reason=row[11],
|
|
575
|
+
record_count=row[12],
|
|
576
|
+
processing_notes=row[13],
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def get_pending_requests(self) -> list[DataSubjectRequest]:
|
|
580
|
+
"""Get all pending requests.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
List of pending requests
|
|
584
|
+
"""
|
|
585
|
+
with self.conn.cursor() as cursor:
|
|
586
|
+
cursor.execute(
|
|
587
|
+
"""
|
|
588
|
+
SELECT id, request_type, data_subject_id, contact_email,
|
|
589
|
+
status, created_at, verified_at, deadline, fulfilled_at,
|
|
590
|
+
reason, verification_method, rejection_reason, record_count,
|
|
591
|
+
processing_notes
|
|
592
|
+
FROM confiture_data_subject_requests
|
|
593
|
+
WHERE status IN (%s, %s, %s)
|
|
594
|
+
ORDER BY deadline ASC
|
|
595
|
+
""",
|
|
596
|
+
(
|
|
597
|
+
RequestStatus.RECEIVED.value,
|
|
598
|
+
RequestStatus.VERIFYING.value,
|
|
599
|
+
RequestStatus.PROCESSING.value,
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
rows = cursor.fetchall()
|
|
603
|
+
|
|
604
|
+
requests = []
|
|
605
|
+
for row in rows:
|
|
606
|
+
requests.append(
|
|
607
|
+
DataSubjectRequest(
|
|
608
|
+
id=row[0],
|
|
609
|
+
request_type=RequestType(row[1]),
|
|
610
|
+
data_subject_id=row[2],
|
|
611
|
+
contact_email=row[3],
|
|
612
|
+
status=RequestStatus(row[4]),
|
|
613
|
+
created_at=row[5],
|
|
614
|
+
verified_at=row[6],
|
|
615
|
+
deadline=row[7],
|
|
616
|
+
fulfilled_at=row[8],
|
|
617
|
+
reason=row[9],
|
|
618
|
+
verification_method=row[10],
|
|
619
|
+
rejection_reason=row[11],
|
|
620
|
+
record_count=row[12],
|
|
621
|
+
processing_notes=row[13],
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
return requests
|
|
626
|
+
|
|
627
|
+
def get_overdue_requests(self) -> list[DataSubjectRequest]:
|
|
628
|
+
"""Get requests that exceeded deadline.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
List of overdue requests
|
|
632
|
+
"""
|
|
633
|
+
with self.conn.cursor() as cursor:
|
|
634
|
+
cursor.execute(
|
|
635
|
+
"""
|
|
636
|
+
SELECT id, request_type, data_subject_id, contact_email,
|
|
637
|
+
status, created_at, verified_at, deadline, fulfilled_at,
|
|
638
|
+
reason, verification_method, rejection_reason, record_count,
|
|
639
|
+
processing_notes
|
|
640
|
+
FROM confiture_data_subject_requests
|
|
641
|
+
WHERE deadline < NOW() AND status != %s
|
|
642
|
+
ORDER BY deadline ASC
|
|
643
|
+
""",
|
|
644
|
+
(RequestStatus.FULFILLED.value,),
|
|
645
|
+
)
|
|
646
|
+
rows = cursor.fetchall()
|
|
647
|
+
|
|
648
|
+
requests = []
|
|
649
|
+
for row in rows:
|
|
650
|
+
requests.append(
|
|
651
|
+
DataSubjectRequest(
|
|
652
|
+
id=row[0],
|
|
653
|
+
request_type=RequestType(row[1]),
|
|
654
|
+
data_subject_id=row[2],
|
|
655
|
+
contact_email=row[3],
|
|
656
|
+
status=RequestStatus(row[4]),
|
|
657
|
+
created_at=row[5],
|
|
658
|
+
verified_at=row[6],
|
|
659
|
+
deadline=row[7],
|
|
660
|
+
fulfilled_at=row[8],
|
|
661
|
+
reason=row[9],
|
|
662
|
+
verification_method=row[10],
|
|
663
|
+
rejection_reason=row[11],
|
|
664
|
+
record_count=row[12],
|
|
665
|
+
processing_notes=row[13],
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return requests
|