# ADR-007 — Chỗ ra quyết định ownership/authz cho Cart & Order là service (có cache Redis), không phải gateway *Tên file: `adr/ADR-007_cho-quyet-dinh-ownership-authz-cart-order.md`* | | | |---|---| | **Status** | `Proposed` | | **Date** | 2026-09-15 | | **Người quyết** | SA + Tech Lead (đề xuất SA, chốt SA+Tech Lead theo `decision-radar.md §5`; Security có quyền phủ quyết vì đây là quyết định authz) | | **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) | | **Điểm radar** | **~6/10** | | **Supersedes** | — | | **Superseded by** | — | | **Liên quan** | `ASR-007` · `QAS-010` · `QAS-003`/`QAS-004` (A4 xung đột dòng 1) · `RBAC_CartCheckout` (BA) §2 · `CMP-02`, `CMP-04`, `CMP-15` | > ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có > `Supersedes: ADR-007` và đổi trạng thái bản cũ thành `Superseded by`. --- ## 1. Bối cảnh Mọi thao tác đọc/sửa/xoá `Cart`/`CartItem`/`Order` phải được đối chiếu quyền sở hữu: Guest theo `session_id` của chính phiên, Customer theo `customer_id` của chính họ — 0% truy cập chéo (IDOR) thành công (`ASR-007`, `QAS-010`). `RBAC_CartCheckout_v1.0.md §2` (BA) đã xác nhận ma trận: cả Guest và Customer đều **không** được xem giỏ hàng/đơn hàng của người khác. Vấn đề kỹ thuật cần chốt: **kiểm tra ownership ở đâu** (gateway hay service) và **có cache không** — vì kiểm tra mỗi request cạnh tranh trực tiếp với ngân sách latency của `QAS-003`/`QAS-004` (đã ghi nhận xung đột ở `QAS §A4` dòng 1). **Ràng buộc đang chi phối:** | Nguồn | Nội dung | |---|---| | `ASR-007` | 100% request qua kiểm tra ownership, 0% IDOR thành công | | `QAS-010` | Test tự động xác nhận 0 lượt IDOR thành công | | `QAS-003`/`QAS-004` | p95 ≤500ms (`GET /v1/cart`), p95 ≤300ms (`PATCH`/`DELETE /v1/cart/items`) — 🔴 số SA đề xuất, `OQ-018`/`OQ-019` chưa xác nhận | | `RBAC_CartCheckout §2` (BA) | Guest theo `session_id`, Customer theo `customer_id` — ma trận đã xác nhận | **Cái đã biết chắc / cái còn là giả định:** | Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng | |---|---|---| | Guest/Customer không được xem giỏ/đơn của người khác | 🟢 Đã kiểm chứng | `RBAC_CartCheckout §2` (BA), đã duyệt từng phần | | Query DB mỗi request để kiểm tra ownership sẽ vi phạm ngân sách latency `QAS-003`/`QAS-004` | 🟡 Ước lượng có cơ sở | Chưa có bài đo thật — cần đo sau khi có thiết kế cụ thể (`QAS §A4`) | | Redis cache session/ownership đủ nhanh để giữ latency trong ngân sách | 🔴 Giả định | Chưa có POC riêng cho cache ownership | ## 2. Phương án đã cân nhắc ### PA-1 — Kiểm tra ownership tại API Gateway/ALB (trước khi vào service) | | | |---|---| | **Mô tả** | Gateway giải mã JWT/session, đối chiếu ownership ngay tại lớp gateway trước khi forward request vào Cart & Order | | **Ưu** | Chặn sớm request không hợp lệ, giảm tải cho service phía sau | | **Nhược** | Gateway (ALB/API Gateway managed) không có quyền truy cập trực tiếp dữ liệu nghiệp vụ (`CartItem` thuộc về ai) — phải gọi ngược lại service hoặc DB để biết ownership, mất lợi thế "chặn sớm"; đặt business logic vào lớp hạ tầng dùng chung vi phạm ranh giới "cái CMP-01 KHÔNG làm" đã ghi ở `SAD §4.1` | | **Chi phí đảo ngược** | Trung bình — chuyển logic xuống service sau này cần tách lại code đã đặt sai lớp | ### PA-2 — Kiểm tra ownership tại service (Cart & Order), query DB mỗi request | | | |---|---| | **Mô tả** | Service tự đối chiếu `session_id`/`customer_id` với DB mỗi request, không cache | | **Ưu** | Đơn giản, luôn chính xác 100% (không có dữ liệu cache cũ) | | **Nhược** | Thêm 1 round-trip DB cho mỗi request — cạnh tranh trực tiếp với ngân sách latency `QAS-003`/`QAS-004` (đã ghi xung đột `QAS §A4`) | | **Chi phí đảo ngược** | Thấp — thêm cache sau này là cải tiến, không phải viết lại | ### PA-3 — Kiểm tra ownership tại service, cache session/ownership trong Redis *(chọn)* | | | |---|---| | **Mô tả** | Service tự đối chiếu ownership; thông tin session/ownership được cache trong Redis (TTL ngắn), chỉ query DB khi cache miss | | **Ưu** | Giữ ranh giới đúng (authz logic ở service sở hữu dữ liệu, không phải gateway); giảm round-trip DB cho phần lớn request — cân bằng được `ASR-007` và `QAS-003`/`004` | | **Nhược** | Thêm thành phần cache cần vận hành (đã có ElastiCache Redis trong kiến trúc — `CMP-15`); cần xử lý đúng khi cache invalidate (VD khi Guest session hết hạn, khi Customer đổi giỏ hàng) | | **Chi phí đảo ngược** | Thấp-trung bình — bỏ cache (quay về PA-2) nếu phát hiện vấn đề nhất quán, không cần viết lại toàn bộ | ### Bảng so sánh | Tiêu chí | PA-1 (Gateway) | PA-2 (Service, không cache) | PA-3 (Service + Redis cache) | |---|---|---|---| | Đúng ranh giới sở hữu logic (`SAD §4.1` "cái CMP-01 KHÔNG làm") | ❌ | ✅ | ✅ | | Đáp ứng ngân sách latency `QAS-003`/`004` | 🔶 Chưa rõ (vẫn cần gọi ngược) | ❌ Rủi ro cao | ✅ (kỳ vọng, chưa đo) | | Độ phức tạp vận hành thêm | Thấp | Thấp nhất | Trung bình (thêm cache) | | Đảm bảo 0% IDOR (`ASR-007`) | ✅ (nếu gọi ngược đúng) | ✅ | ✅ (cần xử lý invalidate đúng) | ## 3. Quyết định > **Chọn PA-3 — Kiểm tra ownership tại service (Cart & Order), cache session/ownership trong > Redis, query DB khi cache miss.** **Vì sao:** Đây là phương án duy nhất giữ đúng ranh giới sở hữu logic (`CMP-01` API Gateway/ALB đã ghi rõ "không tự quyết định authz chi tiết" ở `SAD §4.1`) trong khi vẫn có cơ hội đáp ứng ngân sách latency `QAS-003`/`004` tốt hơn PA-2. **Phạm vi áp dụng:** Toàn bộ endpoint `GET`/`PATCH`/`DELETE /v1/cart*` và các endpoint liên quan `Order` trong phạm vi Cart & Order Service. ### Điều kiện chuyển `Proposed → Accepted` | Điều kiện | Ai xác nhận | Trạng thái hiện tại | |---|---|---| | Đo lại `QAS-003`/`QAS-004` sau khi có thiết kế cache cụ thể (theo `QAS §A4` đã ghi) | QA + Tech Lead | Chưa đo | | Thiết kế cơ chế invalidate cache đúng khi session hết hạn/giỏ hàng đổi | Tech Lead | Chưa thiết kế | | Không bắt buộc POC riêng (radar 6, dưới ngưỡng 8) nhưng khuyến nghị đo trước khi `Accepted` | Tech Lead | — | ## 4. Phương án bị loại và lý do | Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại | |---|---|---|---| | PA-1 — Kiểm tra tại Gateway | Gateway không có quyền truy cập dữ liệu nghiệp vụ, đặt sai ranh giới logic | `SAD §4.1` cột "cái nó KHÔNG làm" của `CMP-01` | Nếu về sau có một BFF (Backend-for-Frontend) layer sở hữu session tập trung — xét lại cho riêng phần xác thực (không phải authz chi tiết theo dữ liệu) | | PA-2 — Service, không cache | Rủi ro vi phạm ngân sách latency `QAS-003`/`004` do round-trip DB mỗi request | `QAS-003`, `QAS-004`, `QAS §A4` | Nếu đo thực tế cho thấy query DB đơn giản (index tốt) đủ nhanh mà không cần cache — đơn giản hoá bằng cách bỏ Redis, giảm 1 thành phần vận hành | ## 5. Hệ quả **Hệ quả tích cực** - Giữ đúng ranh giới sở hữu logic theo domain (khớp `SAD §4.1`) - Có cơ hội đáp ứng ngân sách latency tốt hơn nhờ cache **Hệ quả tiêu cực phải sống chung** - Redis trở thành một điểm phụ thuộc thêm cho luồng Cart & Order — cần thiết kế đường lỗi khi Redis chậm/hỏng (thuộc `FAIL`, hoạt động 8): fallback về query DB trực tiếp khi cache miss/lỗi - Rủi ro dữ liệu cache cũ (stale) nếu invalidate không đúng lúc — cần kiểm thử kỹ **Cái quyết định này khoá lại** | Muốn đổi về sau thì | Tốn | |---|---| | Chuyển sang kiểm tra tại Gateway sau này | Viết lại toàn bộ luồng xác thực + đặt lại ranh giới logic — không khuyến khích | **Việc phát sinh** | Việc | Chủ | Hạn | Ghi ở đâu | |---|---|---|---| | Đo `QAS-003`/`QAS-004` với thiết kế cache cụ thể | QA + Tech Lead | GĐ3 (trước go-live) | `FIT`/bài đo GĐ3 | | Thiết kế cơ chế invalidate cache | SA (hoạt động `sec`) + Tech Lead | GĐ2 tiếp theo | `SEC_e-commerce` §2 | | Thiết kế fallback khi Redis lỗi/chậm | SA (hoạt động `fail`) | GĐ2 tiếp theo | `FAIL_e-commerce` | ## 6. Cách kiểm chứng quyết định này được tuân thủ | Cách kiểm | Công cụ | Chạy ở đâu | `FIT` | |---|---|---|---| | Test IDOR tự động (gọi API với token/session không phải chủ sở hữu) — 0 lượt thành công | Integration test (tương ứng `AC-US003-12/13` BA) | CI + staging trước mỗi release | — (đã có ở `QAS-010`) | | API Gateway (`CMP-01`) không chứa logic authz chi tiết theo dữ liệu | Code review + kiến trúc lint | CI | `FIT-10` (ứng viên) | | Latency `GET`/`PATCH`/`DELETE /v1/cart*` đạt ngân sách sau khi có cache | k6 load test | Staging, trước mỗi release | — (đã có ở `QAS-003`/`004`) | ## 7. Điều kiện xét lại | Dấu hiệu | Ngưỡng | Ai theo dõi | |---|---|---| | Bài đo `QAS-003`/`004` sau khi có cache vẫn không đạt ngân sách | Vượt p95 đề xuất | Tech Lead | | Phát hiện lỗi cache invalidate gây IDOR hoặc dữ liệu cũ ảnh hưởng nghiệp vụ | Bất kỳ sự cố production | Security/SRE | ## 8. Tham chiếu - POC: Không bắt buộc — khuyến nghị đo trước `Accepted` - Bài đo: `QAS-003`, `QAS-004`, `QAS-010` ## 9. Review log (không đổi Status) | Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` | |---|---|---|---|---| | 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có người Security để ký (`ADL §8` việc #7) — ngoại lệ dự án chạy thử không thay được chữ ký chuyên môn Security | > Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên > `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8` > "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 — > `AG2` chưa ký. - Tài liệu ngoài: `RBAC_CartCheckout_v1.0.md §2` (BA) - Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-007`, `QAS_e-commerce_v1.0.md §A4` (xung đột dòng 1)