Ẩn dữ liệu PII đệ quy trong DataWeave: Một hàm duy nhất cho mọi tầng dữ liệu (Và cái bẫy Null)
Một cuộc kiểm toán tuân thủ đã phát hiện dữ liệu PII nằm sâu trong cấu trúc JSON phản hồi API, đòi hỏi giải pháp che giấu toàn diện. Bài viết giới thiệu một hàm đệ quy trong DataWeave để xử lý việc này ở bất kỳ độ sâu nào, đồng thời cảnh báo về lỗi nghiêm trọng do giá trị null gây ra trong môi trường sản xuất.

Một cuộc kiểm toán tuân thủ cách đây không lâu đã phát hiện các giá trị SSN (Số An sinh Xã hội) nằm sâu đến 4 cấp trong phản hồi API của chúng tôi. Giải pháp là sử dụng một hàm đệ quy duy nhất để che giấu tất cả thông tin nhạy cảm này. Tuy nhiên, một "cái bẫy" liên quan đến giá trị null trong môi trường sản xuất đã khiến hàng trăm phản hồi API bị lỗi.
Dưới đây là tóm tắt nhanh:
- Hàm đệ quy maskPII hoạt động dựa trên kiểu dữ liệu: Với Object thì kiểm tra các trường, với Array thì đệ quy, và với kiểu Nguyên thủy (Primitive) thì giữ nguyên.
- Hoạt động hiệu quả ở mọi độ sâu lồng nhau chỉ với một cuộc gọi hàm:
maskPII(payload). - Giá trị null có thể làm sập hệ thống — cần thêm xử lý null tường minh trước trường hợp Object.
- Không cần hardcode đường dẫn — hàm sẽ tự động tìm các trường PII ở mọi cấp độ.
Vấn đề: Dữ liệu PII ở độ sâu không xác định
API sơ đồ tổ chức (org chart) của chúng tôi trả về dữ liệu phân cấp như sau:
{
"company": "Acme Corp",
"ceo": {
"name": "Alice Chen", "ssn": "123-45-6789", "email": "[email protected]",
"reports": [
{
"name": "Bob Martinez", "ssn": "234-56-7890", "email": "[email protected]",
"reports": [
{"name": "Carol Nguyen", "ssn": "345-67-8901"}
]
}
]
}
}
Chúng ta thấy SSN xuất hiện ở cấp độ 1 (CEO), cấp độ 2 (VP), và cấp độ 3 (Giám đốc). Yêu cầu tuân thủ là phải che giấu (mask) TẤT CẢ chúng. Độ sâu thay đổi tùy theo cấu trúc tổ chức — một số đơn vị có thể lên tới 6 cấp.
Giải pháp Đệ quy
Thay vì viết code để truy cập từng đường dẫn thủ công, chúng ta sử dụng tính năng đệ quy của DataWeave:
%dw 2.0
output application/json
fun maskSsn(s: String): String = "***-**-" ++ s[-4 to -1]
fun maskEmail(e: String): String = do { var parts = e splitBy "@" --- parts[0][0] ++ "****@" ++ parts[1] }
fun maskPII(data: Any): Any =
data match {
case obj is Object -> obj mapObject (value, key) ->
if ((key as String) == "ssn") {(key): maskSsn(value as String)}
else if ((key as String) == "email") {(key): maskEmail(value as String)}
else {(key): maskPII(value)}
case arr is Array -> arr map maskPII($)
else -> data
}
---
maskPII(payload)
Cơ chế phân phối kiểu (Type dispatch) hoạt động như sau:
- Object: Kiểm tra từng trường. Nếu là "ssn" hoặc "email", thực hiện che giấu. Nếu không, gọi đệ quy hàm
maskPIIcho giá trị của trường đó. - Array: Áp dụng hàm
maskPIIcho từng phần tử trong mảng. - Nguyên thủy (String, Number, Boolean): Giữ nguyên không thay đổi.
Chỉ với một cuộc gọi duy nhất — maskPII(payload) — chúng ta có thể xử lý dữ liệu ở bất kỳ độ sâu nào.
Cái bẫy Null (The Null Trap)
Trong môi trường sản xuất thực tế, dữ liệu payload (tải trọng) chứa các giá trị null ở cấp độ 3 — ví dụ: một quản lý không cung cấp email. Khối data match ban đầu đã chuyển giá trị null đến... một nơi không mong đợi. Nó cố gắng thực hiện mapObject trên null. Kết quả: Lỗi hệ thống (Crash) và hàng loạt phản hồi API 400 thất bại.
Cách khắc phục là thêm xử lý null một cách tường minh:
fun maskPII(data: Any): Any =
data match {
case is Null -> null
case obj is Object -> obj mapObject ...
case arr is Array -> arr map maskPII($)
else -> data
}
Dòng case is Null -> null sẽ bắt lấy giá trị null trước khi nó chạm đến bộ xử lý Object. Giờ đây, giá trị null sẽ đi qua hệ thống một cách an toàn mà không bị thay đổi.
Kiểm thử các hàm đệ quy
Hiện tại, tôi kiểm thử code với các trường hợp cạnh (edge cases) sau:
- Object rỗng
{}ở từng cấp độ. - Mảng rỗng
[]ở từng cấp độ. - Giá trị
nullở từng cấp độ. - Các kiểu dữ liệu hỗn hợp trong mảng:
["text", 42, null, {"key": "value"}] - Độ sâu tối đa dự kiến (6 cấp đối với sơ đồ tổ chức của chúng ta).
Chỉ mất 5 phút để thiết lập trong DataWeave Playground, nhưng phương pháp này đã ngăn chặn mọi lỗi đệ quy kể từ đó.



