Bỏ qua

Nền cơ sở dữ liệu

Nền quản trị · nhóm 1 · 6 story · 🟡 chưa viết xong · còn 6 mốc

Chưa viết xong: còn 6 mốc

Phần chưa viết hiển thị tiêu chí dự kiến từ kế hoạch (đã qua cổng PO duyệt) và mốc chờ agent làn docs.

Story Service Thứ tự Ghi chú
E-001/1.1 Dòng di trú và schema chung admin-be 6 cap/schema-migration · AD-14 expand-contract, CI chặn vi phạm
E-001/1.2 Ma trận vai và quyền cơ sở dữ liệu admin-be 7 cap/schema-migration · AD-15, AD-20 · phép thử fail-closed trong CI
E-001/1.3 Schema nhật ký bất biến admin-be 8 cap/audit-log · AD-4, AD-18 · cấm cascade, băm móc xích
E-001/1.4 Thu hồi EXECUTE hai hàm vùng nhật ký khỏi PUBLIC admin-be 32 cap/audit-log · mở 11/09/2026 tối (PO chốt — luật nợ sau archive, §G) · nhà của issue có sẵn OAPI-91 (admin-be#28): audit.bam_su_kien(...)audit.dat_bam_moc_xich() chưa REVOKE EXECUTE FROM PUBLIC, cùng lớp lỗ OAPI-83 đã vá ở V7; ca ghim đã có trong TriggerVanNoSauKhiThuHoiExecuteTest · ⛔ không vá ngược change 2.1 đã archive — đổi trailer **Story:** trên body issue thành E-001/1.4 (đó là mối nối epic-status/inbox đọc, ⛔ không phải link.yaml; đính chính 11/09 theo số đo làn portal-be)
E-001/1.5 Cổng ma trận quyền fail-closed với tập cột rỗng — admin-be admin-be 35 cap/schema-migration · mở 11/09/2026 tối (PO chốt OAPI-135, ghi ở AD-15 bổ sung 11/09) · tập cột khai rỗng = ⛔ không cột nào có quyền; chiều cấm đo mức cột · nghiệm thu bằng mutation GRANT UPDATE (ly_do) phá FR-023 phải đỏ (đo 10/09: 5/5 ca xanh) · ⚠️ xếp SAU đường găng GĐ1 (sau 3.3), ⛔ không chen trước 4.1/2.6
E-001/1.6 Hợp đồng CSDL của portal-be fail-closed với tập cột rỗng portal-be 36 cap/tpp-self-registration · mở 11/09/2026 tối (PO chốt OAPI-135) · DatabaseContract/DatabasePrivilegeGuard: nonInsertableColumns() khi tập rỗng trả mọi cột; chiều cấm đo mức cột · nghiệm thu: mutation GRANT INSERT (version) ON app.flyway_schema_history TO portal_app phải làm cổng ném (đo 10/09: không ném, 14/15 ca xanh) · trả luôn nợ OAPI-96 (portal-be#22 — hợp đồng thiếu vùng audit) vì mốc trả của nó là «change kế tiếp chạm DatabaseContract» · ⚠️ xếp SAU đường găng GĐ1

Vì sao làm — BRD

→ Mục tiêu và giá trị nghiệp vụ nằm ở Tổng quan epic. Ở đây chỉ ghi mỗi story góp gì vào mục tiêu đó.

E-001/1.1 Dòng di trú và schema chung — admin-be

Góp gì vào mục tiêu epic. Đây là chỗ câu «mọi thao tác để lại vết không sửa được» thôi là một lời hứa và thành một sự thật do cơ sở dữ liệu cưỡng chế. Khác biệt không nhỏ: NFR-006FR-041 nói nhật ký chỉ-ghi-thêm; nếu điều đó chỉ được giữ bằng mã ứng dụng thì nó là một quy ước, và quy ước thì gỡ được bằng một dòng mã hoặc một biến môi trường.

  • Vùng nhật ký cô lập ở tầng quyền, áp cho CẢ SCHEMA chứ không liệt kê từng bảng. Bảng nhật ký thêm ở E-001/1.3 và các epic sau tự động chịu luật, không cần ai nhớ khai thêm. Liệt kê từng bảng là mô hình sẽ thủng đúng vào ngày có người thêm bảng mới mà quên.
  • Hai dòng di trú chạy bằng HAI danh tính cơ sở dữ liệu khác nhau. Thu hồi UPDATE/DELETE vẫn không chặn DROPTRUNCATE của chủ sở hữu bảng — mà AD-14 vừa giao admin-be cầm danh tính DDL. Một danh tính cầm cả hai vùng là mở đúng cửa sau mà NFR-006 tuyên bố đã khoá. Tách thư mục mà chung vai thì không đóng được gì.
  • Toàn vẹn tham chiếu không được xoá vết kiểm toán. Đây là đường thứ hai, ít ai nghĩ tới: trigger khoá ngoại của PostgreSQL không đi qua kiểm tra quyền, nên ON DELETE CASCADE xoá sạch vết theo dòng cha trong khi REVOKE DELETE vẫn còn nguyên. Cổng đọc trạng thái thật của cơ sở dữ liệu sau khi di trú, không đọc file SQL.
  • Thay đổi cấu trúc chỉ được thêm (expand-contract, AD-14): một phát hành không vừa xoá cột vừa mang mã dùng cột đó. Với hệ thống phải nộp hồ sơ, khả năng lùi lại một phát hành là một yêu cầu vận hành chứ không phải sự tiện tay.

Gỡ nút cho hai làn khác. Làn uat: job db-migrate của AD-26 là ảnh admin-be chạy chế độ chỉ-migrate — chưa có story này thì profile portal của compose không dựng được. Làn portal-be: AD-14.2 đòi nó từ chối khởi động khi phiên bản schema thấp hơn mức cần, mà không có bảng lịch sử di trú thì cổng fail-closed ấy không có gì để đọc.

Vị trí trong hàng đợi. Mười lăm story còn lại của E-001 cộng toàn bộ E-004E-007 xây lên trên ba ranh giới chốt ở đây: schema nghiệp vụ tách schema nhật ký · dòng di trú nhật ký tách dòng di trú ứng dụng (ngoại lệ có chủ đích của AD-14, ghi ra để nó là quyết định chứ không phải kẽ hở) · quy ước đặt tên và kiểu dữ liệu. Sai ở đây thì mọi thứ sau xây trên một nền lệch.

E-001/1.2 Ma trận vai và quyền cơ sở dữ liệu — admin-be

Góp gì vào mục tiêu epic. Trước story này, quyền của hai vai ứng dụng trên cơ sở dữ liệu rải ở hai chỗ (bootstrap cấp mặc định, dòng di trú cấp tường minh) và không chỗ nào đọc được thành một ma trận để đối chiếu với ý định của kiến trúc. Thẩm định viên hỏi «vai của cổng bên thứ ba có gì trên bảng X» thì phải đọc chéo nhiều file rồi suy — đúng câu hỏi mà AD-20 nói ma trận tồn tại để trả lời.

  • Story này trả bốn món nợ hai change trước đã ghi tên, ⛔ không phải một story mới: ma trận vai đầy đủ · vai ứng dụng thừa DELETE trên bảng dùng chung · cô lập theo tổ chức chưa có lớp chặn thật · bí mật fail-open ở production (món cuối cố ý để lại — thuộc AD-27, vành đai khác).
  • Và nó phát hiện một lỗi nặng hơn cả bốn món. Repo đang chạy bằng vai app_runtime — tên admin-be tự nghĩ — trong khi kiến trúc (AD-20) và tiêu chí của story đều khai admin_app. AD-15 mô tả đúng ca này ở mục Prevents: thu hồi quyền trên tên vai tự nghĩ, đội triển khai cấp cho vai khác quyền ALL, và không repo nào bắt được — vì chính cơ chế lẽ ra bắt (ma trận) là thứ story này mới dựng.
  • Lần thứ hai (11/09) nó gác được thật. Bảng lịch sử di trú vùng nhật ký nằm ngoài ma trận ⇒ cả hai vai ứng dụng thừa INSERT mà cổng khởi động không thấy; portal-be từ chối khởi động trên cụm UAT vì hợp đồng của nó chặt hơn. Sửa ở admin-be (PO chốt: ⛔ không nới hợp đồng portal-be) — thu quyền và đưa bảng vào ma trận để từ nay cổng gác.

Vị trí trong hàng đợi. Thứ tự 7; đứng trên E-001/1.1 (tách danh tính di trú) và tpp-user-account. Gồm hai change: db-role-matrix (archive 07/09) dựng ma trận và đổi tên vai; audit-history-thu-quyen (archive 11/09) vá lỗ bảng ngoài ma trận. Lớp chặn tổng quát «bảng nào không có dòng ma trận ⇒ đỏ» giao E-001/1.5.

E-001/1.3 Schema nhật ký bất biến — admin-be

Góp gì vào mục tiêu epic. Story này là chỗ câu «nhật ký không sửa được» thôi là một lời hứa và thành một câu hỏi trả lời được. Khoảng cách giữa hai thứ đó chính là thứ thẩm định viên đến để đo.

  • «Không ai sửa được» và «không ai sửa được mà không để lại dấu» là hai câu khác nhau. Trước story này, hệ thống chỉ nói được câu thứ nhất, và nói được có điều kiện: mọi vai ứng dụng đã bị thu hồi quyền sửa và xoá, nhưng chủ sở hữu vùng nhật ký vẫn còn trọn quyền — mà đó là danh tính tiến trình dùng ở mỗi lượt di trú. Với một người thẩm định, đó là một lời khai còn hở. Băm móc xích lấp đúng chỗ hở ấy: nó không cấm được người có quyền cao nhất, nhưng nó làm mọi lần sửa một bản ghi đã có để lại vết không xoá kịp.
  • Một đường mất vết mà quyền hạn không chặn được. NFR-006FR-041 đòi nhật ký chỉ ghi thêm; nhưng nếu một bảng nghiệp vụ có ràng buộc «xoá kéo theo» trỏ vào vùng nhật ký, thì xoá một dòng nghiệp vụ sẽ kéo theo vết kiểm toán — và cơ chế toàn vẹn tham chiếu của cơ sở dữ liệu không đi qua bước kiểm quyền. Tức lệnh thu hồi quyền xoá còn nguyên mà vết vẫn mất. Story này đóng đường đó bằng một cổng máy hỏi thẳng cơ sở dữ liệu, không hỏi tệp mã.
  • Nó phải đứng TRƯỚC mọi thao tác có gì để ghi. Luồng đăng nhập quản trị và phân quyền người dùng bắt buộc ghi nhật ký từng thao tác. Dựng vùng nhật ký sau khi đã có thao tác nghĩa là có một quãng thao tác được ghi bằng luật lỏng hơn — và quãng đó không sửa lại được, vì chính nhật ký là thứ không cho sửa.

Vị trí trong hàng đợi. Thứ tự 8, khép lại nhóm nền cơ sở dữ liệu. Nó tiêu thụ schema riêng và ma trận quyền của E-001/1.11.2, rồi làm nền cho E-001/4.1 (ghi nhật ký thao tác quản trị) và 4.2 (tra cứu nhật ký) ở nhóm 4.

E-001/1.4 Thu hồi EXECUTE hai hàm vùng nhật ký khỏi PUBLIC — admin-be

⟨CẦN NGƯỜI VIẾT — agent làn admin-be viết khi đóng change⟩

E-001/1.5 Cổng ma trận quyền fail-closed với tập cột rỗng — admin-be — admin-be

⟨CẦN NGƯỜI VIẾT — agent làn admin-be viết khi đóng change⟩

E-001/1.6 Hợp đồng CSDL của portal-be fail-closed với tập cột rỗng — portal-be

⟨CẦN NGƯỜI VIẾT — agent làn portal-be viết khi đóng change⟩

Ai dùng, dùng thế nào — URD

Không có bề mặt người dùng

Nhóm này chỉ có phần máy chủ hoặc hạ tầng — không cần URD (khong-can, có vết ở đây). Hành trình người dùng nếu có nằm ở nhóm có màn tương ứng.

Hệ làm gì, ràng buộc gì — PRD

Mỗi story một mục, phần máy chủ trước, giao diện sau. Story chưa đóng hiển thị tiêu chí dự kiến từ epics.md.

E-001/1.1 Dòng di trú và schema chung — admin-be

Nguồn hồ sơ. admin-be @ origin/main — change đã đóng; hồ sơ bản cuối ở openspec/changes/archive/2026-09-07-schema-migration/ (proposal.md · design.md · specs/schema-migration/spec.md · specs/scaffold-admin-be/spec.md · tasks.md 43/43). Issue OAPI-9-dong-di-tru-va-schema-chung (thangvv111/admin-be#4).

Bản thảo này viết từ nhánh 94cfddb và change được archive trong lúc viết. Đối chiếu bản archive: proposal.mdspec.md giống hệt; tasks.md tick nốt ba mục; design.md thêm hẳn một mục «Review outcome» — phần dưới đã cập nhật theo bản cuối.

Change thứ hai của story — giao-dich-that (OAPI-159-giao-dich-no-op-toan-repo, thangvv111/admin-be#57) — origin/giao-dich-that @ 57afc8f (artifact 2fc497c), đang mở, docs: khong-can. Đóng nợ đo 11/09: bốn bean DataSource không cái nào @Primary ⇒ mọi @Transactional từng là chú thích trang trí; lỗi đã vá ở audit-action-event, change này chỉ đóng cổng thủng — ⛔ không đổi src/main, không di trú, không đổi gì người dùng thấy. Hai requirement thêm vào spec: (1) scaffold-admin-be «giao dịch của ứng dụng có thật» — đúng một DataSource @Primary là bean dataSource, PlatformTransactionManager gắn chính bean ấy, kết nối trong giao dịch có current_user = admin_app (⛔ không phải chủ bảng flyway_app — nếu không mọi REVOKE của AD-18/AD-20 vô nghĩa mà lưới vẫn xanh), mọi bean có @Transactional là proxy (quét toàn context, ⛔ không liệt kê tay); (2) approval-workflow «mỗi thao tác ghi của khung là một giao dịch nguyên tử» — moDeXuat · suaNoiDung · quyetDinh (từ chối/duyệt): lượt ghi sau đổ ⇒ lượt trước cuộn lại, không vết. Đo bằng hai lớp lưới mới GiaoDichCauHinhTest · GiaoDichKhungPheDuyetTest. Phần yêu cầu/ràng buộc của story bên dưới không đổi.

Yêu cầu và cách đo

Yêu cầu Ràng buộc Đo bằng
Dòng di trú duy nhất, tách theo vùng Chỉ admin-be di trú lên CSDL dùng chung; hai dòng độc lập (nghiệp vụ · nhật ký) chạy bằng hai danh tính khác nhau; danh tính nghiệp vụ không có quyền định nghĩa cấu trúc trên vùng nhật ký Di trú trên CSDL sạch ⇒ hai vùng, mỗi vùng một lịch sử; danh tính nghiệp vụ thử tạo/xoá bảng vùng nhật ký ⇒ bị từ chối ở tầng CSDL
Vùng nhật ký chỉ cho thêm mới Mọi vai ứng dụng lúc chạy không sửa/xoá được bản ghi nhật ký, khôngDROP/TRUNCATE ở bất kỳ vùng nào; luật áp cho cả schema, không liệt kê bảng Bảng nhật ký tạo SAU khi luật đã áp vẫn chịu luật mà không khai thêm
Toàn vẹn tham chiếu không xoá vết Không khoá ngoại nào chạm vùng nhật ký dùng xoá lan truyền hoặc gán rỗng Truy vấn trạng thái thật của CSDL sau khi di trú — ⛔ không grep file SQL
Thay đổi cấu trúc chỉ được thêm Một phát hành không vừa xoá cột/đổi tên vừa mang mã dùng chúng; ca bắt buộc phá luật phải ghi tường minh trong chính tệp di trú Tệp di trú có lệnh xoá cột mà thiếu ghi chú cho phép ⇒ cổng đỏ, nêu đích danh tệp
Chế độ chỉ chạy di trú rồi thoát Áp di trú xong kết thúc tiến trình, không mở bề mặt phục vụ; mã thoát 0 khi thành công, khác 0 khi thất bại Chạy trên CSDL sạch: không cổng phục vụ nào được mở
Hợp đồng phân trang và xung đột trạng thái page đánh số từ 1; page < 1 bị từ chối bằng phong bì lỗi ADM.*; thao tác đổi trạng thái lặp lần hai khi đã ở trạng thái đích trả mã xung đột, ⛔ không âm thầm coi là thành công Ba ca qua HTTP thật

Quyết định và cái phải trả

  • Hai schema app / audit, hai vai chạy di trú flyway_app / flyway_audit. Vai ứng dụng lúc chạy (app_runtime) không sở hữu bảng nào: đủ quyền đọc-ghi trên app, nhưng trên audit chỉ SELECTINSERT. Ứng dụng không bao giờ kết nối bằng vai Flyway.
  • ⚠️ Bẫy đã ghi tên: ALTER DEFAULT PRIVILEGES chỉ áp cho đối tượng do vai nêu trong FOR ROLE tạo ra. Khai nhầm vai thì luật có mà không ăn, và không có gì báo. Nên nó phải khai cho flyway_audit — đúng vai sẽ tạo bảng nhật ký ở E-001/1.3. Phép thử fail-closed vì thế phải chạy trên bảng tạo sau khi luật đã áp, không phải bảng có sẵn.
  • Cổng khoá ngoại là truy vấn trên CSDL thật, không phải grep. Đây là bài học đã trả giá một lần ở lượt trước: phép kiểm đếm chuỗi con trên file SQL đọc y như một cổng, nhưng nó không biết trạng thái cuối.
  • Cổng expand-contract thì NGƯỢC LẠI — cố ý là phép kiểm trên văn bản. Thứ cần chặn ở đây là ý định trong phát hành này, không phải trạng thái cuối. Cái phải trả được nói thẳng: nó mang đúng điểm yếu mà cổng khoá ngoại tránh. Bù lại, chuẩn hoá bỏ chú thích trước khi quét, và cửa thoát là một dòng khai tường minh phải có người viết ra — phá luật thành hành vi cố ý ghi lại được, không phải trượt tay.
  • Chế độ chỉ-migrate là một profile, thoát bằng lệnh thoát của khung — chạy di trú, in phiên bản đạt được, thoát 0; lỗi thì khác 0; không mở cổng HTTP. Đúng hình dạng job db-migrate cần.
  • Phân trang một-gốc chốt ở CẢ cấu hình LẪN mã. Spring Data mặc định đếm từ 0, nên thiếu cấu hình thì page=1 lặng lẽ trả trang thứ hai — sai mà không ai thấy. AD-28 đòi ca lưới page=0 bị từ chối, tức phải kiểm cả chiều âm chứ không chỉ chiều thuận.

⛔ Cố ý KHÔNG có trong story này

  • Ma trận vai đầy đủ (AD-15, AD-20) — E-001/1.2. Ở đây chỉ dựng đúng số vai cần để tách quyền DDL; phân quyền chi tiết theo nghiệp vụ là story sau.
  • Bảng nhật ký cụ thể và băm móc xíchE-001/1.3. Ở đây dựng schema nhật ký và luật quyền áp cho cả schema, nên bảng thêm sau tự động chịu luật.
  • Mọi bảng nghiệp vụ; cổng phiên bản schema tối thiểu của portal-be (thuộc repo portal-be).

⚠️ Người duyệt cần biết

1. Verdict là TỰ CHẤM, không phải review độc lập. Hồ sơ ghi Verdict: PASS @ 46fec3f — TỰ CHẤM, knob openapi.review.enforce=self, PO chốt 07/09/2026. Entry board nói rõ chưa có phiên độc lập; CheckMate chấm lại ở cùng SHA. Đây là verdict tạm có dấu, và hồ sơ tự khai như vậy — người duyệt cần đọc nó đúng mức đó.

2. Điều đáng nhớ nhất của change này là một lần suýt lọt, và nó lọt qua MỌI cổng. Hồ sơ ghi thẳng: trước lượt lens review, 61/61 phép thử xanh, CI xanh đủ cổng — và cơ chế bí mật của repo đang hỏng. Change đã xoá mất spring.config.import: configtree, tức cách đọc bí mật duy nhất theo AD-27, thứ mà chính story khung dựng lên và đã tick ✅. Không phép thử nào bắt được, vì phép thử fail-closed tự truyền tham số đó trên dòng lệnh — guard bị làm mù bởi chính harness của nó. ⇒ Bài học đã thành câu tự kiểm cho mọi ô tick về sau: «gỡ cơ chế này ra thì ca nào ĐỎ?» — không trả lời được bằng tên một phép thử cụ thể thì ô tick đó chưa mang tải.

3. Bốn bất biến rút ra, ràng buộc E-001/1.21.3:

  • Một guard chỉ có nghĩa ở môi trường nó thật sự chạy. Toàn bộ luật quyền từng sống trong một bootstrap mặc định TẮT, mà cả 74 phép thử đều bật nó — tức chứng minh luật trong một cấu hình production không dùng. Cách đóng: hỏi trạng thái thật lúc khởi động, không tin cấu hình.
  • Cổng CI phải chạy đúng hình dạng triển khai thật. Cổng «container khoẻ» từng chạy ảnh bằng superuser — tức chứng minh đúng hình dạng mà luật cô lập nhật ký cấm.
  • ALTER DEFAULT PRIVILEGES chỉ áp cho vai nêu trong FOR ROLE, và CREATE SCHEMA IF NOT EXISTS bỏ qua AUTHORIZATION khi schema đã tồn tại. Cả hai fail-open lặng lẽ, và Testcontainers luôn cấp CSDL sạch nên không phép thử nào thấy được ca thứ hai.
  • Phép thử hai chiều phải gỡ TRỌN lớp chặn. Nới GRANT mà vẫn để REVOKE chạy sau ⇒ vẫn xanh ⇒ kết luận ngược dấu.

4. Năm chỗ còn hở, hồ sơ tự ghi có tên — người duyệt nên coi đây là danh sách phải theo, không phải phần trang trí:

Còn hở
S7.3 Ảnh dùng chung cho hai chế độ ⇒ bí mật vai Flyway có mặt cả ở chế độ phục vụ
S8.4 Thiếu ca đối xứng: flyway_audit không đụng được vùng nghiệp vụ
S9.1 POST …/run chưa ghi nhật ký kèm traceId, và bảng sự kiện schema chưa có mã production nào ghi vào
Cửa thoát expand-contract miễn theo TỆP chứ không theo câu lệnh
Phép thử dùng chung container để lại bảng rác ở vùng nhật ký

5. Đường chạy DDL qua HTTP đã được giữ lại. POST /api/v1/schema/migrations/run vẫn nằm trong bản cuối, mặc định TẮT, bật ở môi trường tích hợp và trong test. Phương án thay thế (bỏ route, đánh ca 409 là [N/A] tới E-001/3.1) không được chọn. ⇒ Ghép với S9.1 ở trên: một đường đổi trạng thái schema, chạy trong repo chưa có xác thực, và chưa ghi nhật ký kèm traceId. Ba mệnh đề đó đều đúng cùng lúc, và người duyệt nên cân chúng cùng lúc.

6. Hệ quả vận hành cho làn uat. Chế độ chỉ-migrate đã có và đã đo mã thoát 0 trên CI. ⚠️ Nhưng ảnh admin-be nay cần cơ sở dữ liệu mới chạy được ⇒ compose phải dựng db trước admin-be, và db-migrate trước cả hai. Làn portal-be đã có bảng lịch sử di trú để khai phiên bản schema tối thiểu.

  • Tài liệu hướng dẫn: khong-can — quyết định ghi Ở ĐÂY, không ở link.yaml. Change 2026-09-07-schema-migration (dòng di trú, schema và vai cơ sở dữ liệu; bề mặt /api/v1/schema/* là của người vận hành và mặc định TẮT) không sinh trang hướng dẫn người dùng cuối. Làn docs quyết sau khi đọc hồ sơ; PO duyệt 07/09/2026.

    ⚠️ Khoá docs: trong link.yaml của change này để trống, và sẽ ở nguyên như vậy: hồ sơ đã archive, mà PO chốt 07/09/2026 — archive là BẤT BIẾN. Hệ quả nói thẳng để không ai dò lại: inbox --role docs mục [D3] sẽ kê change này vĩnh viễn. Đó là lỗ thật đã có người quyết, không phải việc còn tồn — và dòng này là vết của quyết định đó.

E-001/1.2 Ma trận vai và quyền cơ sở dữ liệu — admin-be

Nguồn hồ sơ. admin-be @ origin/main 6ac16cbhai change đã đóng, đều khai story: E-001/1.2:

Change Issue Hồ sơ
2026-09-07-db-role-matrix OAPI-28-db-role-matrix (thangvv111/admin-be#8) proposal · design · specs/schema-migration/spec.md · tasks · test-cases · security
2026-09-11-audit-history-thu-quyen OAPI-150-audit-history-thu-quyen (thangvv111/admin-be#54) như trên, cùng capability

Cả hai ở openspec/changes/archive/bản cuối. sha7 cho dấu duyệt: docs-gate.sh sha <slug>admin-be, tính riêng từng change.

Yêu cầu và cách đo

Yêu cầu Ràng buộc Đo bằng
Ma trận vai × bảng × quyền là một artifact khai báo, một nguồn (AD-15) File máy đọc được trong admin-be, cạnh dòng di trú; dòng di trú cấp theo nócổng khởi động sinh phép kiểm từ nó; quyền ⛔ không khai ở nơi nào khác. Ma trận khai đủ hàng AD-20, kể cả bảng chưa tồn tại Thêm một dòng ma trận ⇒ cổng tự kiểm dòng đó, ⛔ không sửa mã cổng; dòng của bảng chưa có ⇒ bỏ qua có ghi log, ⛔ không tính đạt
Tên vai ứng dụng khớp ma trận admin_app · portal_app đúng AD-20; đổi app_runtimeadmin_app (⛔ không giữ tên cũ kèm ánh xạ). Tiến trình từ chối khởi động nếu kết nối bằng vai không có trong ma trận Cấu hình vai lạ ⇒ không khởi động, thông điệp nêu đích danh tên vai
Vai ứng dụng ⛔ không xoá trên nhóm bảng dùng chung Hàng đầu AD-20: cả hai vai SELECT, INSERT, UPDATE, ⛔ không DELETE/TRUNCATE Vai trang quản trị thử xoá tài khoản bên thứ ba ⇒ CSDL từ chối vì thiếu quyền
Quyền ⛔ không cấp trước cho bảng chưa tồn tại Gỡ ALTER DEFAULT PRIVILEGES cấp sẵn cho vai ứng dụng ở vùng nghiệp vụ; mọi quyền cấp tường minh từng bảng. ⛔ Giữ nguyên câu thu hồi trước của vùng nhật ký (AD-4.1) — cùng hình, ngược mục đích Tạo bảng mới bằng danh tính di trú ⇒ không vai ứng dụng nào tự có quyền (cả admin_app)
Cổng khởi động kiểm mọi vai, cả hai chiều Với mỗi dòng ma trận mà bảng đã tồn tại: kiểm chiều cấm và chiều phải có; lệch chiều nào cũng từ chối khởi động, nêu vai · bảng · quyền. Dòng cấp theo cột dùng has_column_privilege (bài học tpp-user-account) Vai thừa một quyền cấm ⇒ đỏ đích danh; thiếu một quyền phải có ⇒ đỏ đích danh
Lối đọc có lọc cho bảng mang cột tổ chức Ma trận phân biệt SELECT trên bảng với SELECT qua lối đọc có lọc; cổng cưỡng chế «có lối đọc ⇒ ⛔ không SELECT trần» Đọc dòng ma trận của bảng có cột tổ chức ⇒ khai lối đọc có lọc cho vai cổng bên thứ ba
Bảng lịch sử di trú vùng nhật ký ⛔ không ghi được bởi vai ứng dụng, và có mặt trong ma trận (change 11/09) Thu INSERT khỏi cả portal_app admin_app; admin_app giữ SELECT (đối xứng AD-14.2), portal_app ⛔ không quyền nào. Thu ở cả hai nơi: di trú vùng audit (V4) khối REVOKE có điều kiện trong bootstrap has_table_privilege + hành vi dưới vai thật cho cả hai vai; khởi động lần hai vẫn xanh (ca tất định); xoá một trong hai dòng ma trận ⇒ ca ghim đỏ

⛔ Cố ý KHÔNG có trong story này

Không Row-Level Security cho cô lập theo tổ chức — cần phiên mang danh tính tổ chức (E-001/2.2, 2.3). ⛔ Không vá bí mật fail-open ở production — thuộc AD-27, trộn vào là gộp hai vành đai vào một change. ⛔ Không chạm vùng nhật ký nghiệp vụ (E-001/1.3) hay INTERNAL_USER (E-001/2.1). ⛔ Không đối chiếu ma trận ↔ spine bằng máy (spine là văn xuôi; lệch là finding của review). ⛔ Không đổi default ACL vùng audit cho bảng sự kiện tương lai (vẫn cần INSERT cho portal_app, AD-20 hàng cuối). ⛔ Không chặn tổng quát «bảng ngoài ma trận ⇒ đỏ» — E-001/1.5.

⚠️ Người duyệt cần biết — sáu chỗ

1. Đổi tên vai là THAY ĐỔI PHÁ VỠ với mọi môi trường đang chạy — và không có đường êm. ci.yml, ảnh, biến môi trường, tên file bí mật, và repo uat đều phải đổi; ALTER ROLE … RENAME ⛔ không dùng được vì vai tạo bởi bootstrap ở mỗi môi trường và PostgreSQL từ chối đổi tên vai đang có kết nối. Vai cũ app_runtimekhông tự xoá — được gỡ hết quyền và đặt NOLOGIN qua khối «đường nâng cấp» của bootstrap, xoá hẳn là bước vận hành của DBA ghi ở README. Làn uat được báo trước khi merge.

2. «Gỡ câu» ⛔ KHÔNG bằng «gỡ quyền» — chỗ bản đầu làm sai, lens review bắt. ALTER DEFAULT PRIVILEGEStrạng thái bền trong pg_default_acl. Xoá dòng khỏi bootstrap chỉ ngăn CSDL mới nhận nó; CSDL đã chạy bản cũ (UAT, production khi có) vẫn tự cấp trọn bốn quyền cho vai cũ trên mọi bảng tương lai. ⇒ Khối «đường nâng cấp» thu đúng bản ghi ACL. ⚠️ Cơ chế này chỉ chạy khi có người áp lại file — không cổng nào trong repo đo được, suite mù vì Testcontainers luôn cấp CSDL sạch. Bước vận hành ghi ở READMEsecurity.md «Còn hở» mục 7.

3. Cùng họ lỗi, lần hai (11/09): bootstrap CHẠY LẠI mỗi lần khởi động và CẤP LẠI. Bản đầu của change vá viết «thu ở di trú, ⛔ không ở bootstrap — bootstrap chạy trước Flyway nên bảng chưa tồn tại». Sai một nửa, đo được ngay ở lượt lưới đầu: lớp test thứ hai trong cùng container thấy portal_app vẫnINSERT. «Bootstrap chạy trước Flyway» chỉ đúng lượt dựng đầu; mọi lượt sau nó chạy sau mọi migration đã có. ⇒ Thu ở cả hai nơi; gỡ một là hở ở một môi trường (UAT chạy bootstrap tắt — ở đó V4 là đường có hiệu lực; local/CI thì ngược lại).

4. Cô lập theo tổ chức (T_iso) — security.md khai ĐÚNG MỨC, ⛔ không tick ✅, và người duyệt ⛔ đừng đọc cao hơn. Bản đầu tự thêm ô «đọc qua lối lọc» cho app.tpp_user không có nguồn (AD-20 hàng 1 cấp SELECT trần cho cả hai vai) — đã gỡ. Trạng thái thật: chưa đóng gì trên bảng đang tồn tại; cái đóng được là luật cho bảng sinh về sau (application, registration) có cơ chế cưỡng chế. Nửa còn lại — neo tham số lọc vào danh tính đã xác thực — hàm nhận p_tpp_id từ bên gọi, bên gọi truyền gì lọc theo đó; chỉ đóng được khi có phiên mang danh tính tổ chức.

5. Bảng nằm ngoài ma trận là bảng cổng MÙ — và ma trận sai thì cổng sai theo, đó là chủ ý. Cổng sinh phép kiểm từ ma trận, nên ma trận là nguồn duy nhất (AD-15); muốn cổng độc lập với nguồn là phá chính AD-15. Hệ quả: một bảng không có dòng thì cổng không thấy — đúng lỗ bảng lịch sử di trú rơi vào 11/09. Chặn tổng quát thuộc E-001/1.5 (chưa có issue).

6. Hai lỗ có tên còn mở, ghi ra chứ không lặng. (a) Quyền EXECUTE trên hàm lọc ⛔ không cổng nào gác — ma trận khai quyền trên bảng, không trên hàm; ca tương ứng @Disabled có ghi lý do. (b) audit.schema_event — bảng sự kiện vùng nhật ký — chưa bao giờ có dòng nào ở môi trường thật (làn uat phát hiện, đo git grep: ba chỗ, không chỗ nào INSERT); nó đang là mốc để hỏi quyền, chưa là nhật ký. Cả hai change tự chấm (review.enforce=self), CheckMate chấm lại sau.

E-001/1.3 Schema nhật ký bất biến — admin-be

Nguồn hồ sơ. Story này hoàn thành qua hai change đã đóngadmin-be @ origin/main: openspec/changes/archive/2026-09-08-nhat-ky-bam-moc-xich/ (chính — issue OAPI-42-schema-nhat-ky-bat-bien) và .../2026-09-07-comment-csdl-dinh-chinh/ (đính chính comment cơ sở dữ liệu). Vật mang trên trunk: src/main/resources/db/migration/audit/V3__bam_moc_xich.sql.

Năm vế tiêu chí, và vế nào về lúc nào

Tiêu chí Về ở đâu
Nhật ký ở schema riêng; quyền sửa và xoá bị thu hồi cho cả schema, kể cả bảng chưa ra đời đã có từ lượt dựng nền của nhóm này
Quyền tạo bảng tách khỏi vai di trú của schema nghiệp vụ; di trú nhật ký là dòng riêng, danh tính riêng đã có từ lượt dựng nền
Vai của cổng bên thứ ba ghi được, ⛔ không đọc được vùng nhật ký đã có từ lượt cấp quyền
⛔ Cấm «xoá kéo theo» và «xoá thành rỗng» trên mọi khoá ngoại chạm vùng nhật ký story này
Mỗi bản ghi mang băm móc xích với bản ghi trước story này

Yêu cầu và cách đo

Yêu cầu Ràng buộc Đo bằng
Băm tính ở tầng cơ sở dữ liệu, không ở tầng ứng dụng Trigger tự tính khi chèn; giá trị bên ghi truyền vào bị bỏ vô điều kiện. Bên ghi tự chọn được băm thì móc xích chỉ chứng minh bên ghi nhất quán với chính nó — tức chống tai nạn, không chống sửa có chủ ý Ca chèn kèm băm giả ⇒ giá trị bị ghi đè
Băm phủ toàn bộ nội dung dòng, và phủ cả băm dòng trước ⛔ Thiếu băm dòng trước thì đổi chỗ hai dòng mà cả hai băm vẫn đúng — chuỗi thôi là chuỗi. ⛔ Bỏ sót một cột nghiệp vụ là để lại đúng một chỗ sửa được mà móc xích không thấy; thêm cột về sau phải thêm vào phép băm Ca lưới canh riêng việc «thêm cột mà quên đưa vào phép băm»
Mốc thời gian đưa vào phép băm ở dạng chuẩn hoá theo UTC ⛔ Không dựa vào định dạng mặc định của phiên — hai phiên khác cấu hình sẽ tính ra hai băm khác nhau cho cùng một dòng, và móc xích mất khả năng kiểm lại ở máy khác, năm khác Hàm băm khai IMMUTABLE: cùng đầu vào luôn cùng kết quả, không đọc trạng thái phiên
Chuỗi đi theo thứ tự GHI, ⛔ không theo thứ tự XẢY RA Thứ tự ghi do cơ sở dữ liệu cấp và trigger ghi đè, có ràng buộc duy nhất. Mốc sự kiện giữ nguyên nghĩa của nó và vẫn nằm trong phép băm Hai ca: chèn dòng mang mốc sự kiện cũ hơn dòng cuối ⇒ chuỗi không đứt; và trigger ghi đè cả thứ tự ghi bên ghi truyền vào
hàm kiểm toàn vẹn trả về bản ghi đầu tiên làm đứt chuỗi Hàm chỉ đọc, chạy bằng quyền người gọi ⛔ không phải quyền chủ sở hữu — hàm chạy quyền chủ sở hữu sẽ cho một vai không có quyền đọc vùng nhật ký đọc gián tiếp nội dung qua thông báo lỗi, tức đi vòng qua chính ma trận quyền Ca dựng một lần sửa rồi hỏi hàm ⇒ chỉ đúng dòng đầu tiên đứt
Cổng khoá ngoại hỏi cơ sở dữ liệu, ⛔ không đọc tệp .sql Quét cả hai chiều — bảng nhật ký trỏ ra, và bảng ngoài trỏ vào nhật ký. Chiều thứ hai mới là chiều nguy Ca đọc tệp sẽ xanh cả khi di trú không chạy và xanh cả khi ai đó thêm ràng buộc bằng tay ngoài dòng di trú ⇒ phải hỏi siêu dữ liệu thật

Dòng dữ liệu đã có trước khi cơ chế ra đời được tính băm ngay trong lượt di trú, theo đúng thứ tự chuỗi — không làm vậy thì chuỗi bắt đầu bằng một khoảng trống và cổng đỏ vì lịch sử, không vì ai sửa gì.

⚠️ Ba giới hạn được khai đúng mức, không khai quá

  1. Chủ sở hữu vùng nhật ký vẫn tắt được trigger rồi chèn dòng với băm tuỳ ý. Móc xích không ngăn điều đó. Cái nó bảo đảm hẹp hơn và vẫn đáng giá: mọi lần sửa hay xoá một dòng đã có đều làm chuỗi đứt, và kẻ sửa phải tính lại băm của toàn bộ dòng phía sau — vẫn làm được bằng quyền cao nhất, nhưng nó không còn là một câu lệnh sửa. Đóng hẳn cần ký số bằng khoá nằm ngoài cơ sở dữ liệu, hoặc neo băm định kỳ sang một hệ thống khác.
  2. ⚠️ Móc xích không hồi tố. Băm của dòng cũ được tính tại lượt di trú, nên nó chứng minh «từ lúc này trở đi không ai sửa», ⛔ không chứng minh «chưa ai sửa trước đây».
  3. ⚠️ Chi phí của trigger chưa đo được thật — mỗi lượt ghi phải đọc dòng mới nhất. Chưa có đường ghi nào (E-001/4.1 chưa làm) nên chưa đo; ghi ra thay vì giả định.

⚠️ Người duyệt cần biết — ba chỗ

1. Một lỗ NGHIÊM TRỌNG do chính làn thi công tự soi bắt được, đáng đọc vì loại của nó. Bản đầu cho chuỗi đi theo thứ tự xảy ra. Nhưng mốc sự kiện chỉ có giá trị mặc định, không cấm bên ghi truyền vào; một vai có quyền ghi chèn dòng mang mốc cũ hơn là thao tác hợp lệ mà làm chuỗi đứt oan. Hậu quả thứ hai tệ hơn hậu quả thứ nhất: kẻ muốn che một lần sửa thật chỉ cần chèn một dòng lùi ngày để cổng luôn đỏ, rồi không ai phân biệt được đỏ thật với đỏ giả — tức lỗ đó vô hiệu hoá được chính cơ chế. Đã sửa sang thứ tự ghi do cơ sở dữ liệu cấp, kèm hai ca lưới canh.

2. Một comment cột còn đọc theo bản cũ. Comment của cột băm-dòng-trước vẫn ghi chuỗi đi theo «thứ tự mốc sự kiện và định danh», trong khi cơ chế đã chuyển sang thứ tự ghi. Mã chạy đúng; chỗ lệch nằm ở lời khai đọc được ngay trong cơ sở dữ liệu — đúng họ lỗi mà repo này đã bắt nhiều lần, và là thứ người thẩm định đọc trước khi đọc mã.

3. Một câu hỏi CHƯA CÓ NGƯỜI TRẢ. Giới hạn số 1 ở trên được ghi vào mục «còn hở» của hồ sơ với ghi chú «người trả: chưa có» — cần PO quyết đưa vào việc tồn tuân thủ hay chấp nhận rủi ro. Nó nằm ngoài phạm vi story này và sẽ không tự nổi lên ở story nào khác.

Về khoá docs: — change chính khai một trang hướng dẫn người dùng, với lý do ghi thẳng trong hồ sơ: «change này đổi hình dạng bảng nhật ký, thứ thẩm định viên đọc». Người đọc mà chính change nêu tên là thẩm định viên, tức người đọc của vùng này; còn vùng hướng dẫn dành cho nhân viên ngân hàng thao tác trên màn hình, mà chưa story nào cấp màn nhật ký — khối «nhật ký thao tác» của trang chủ quản trị thuộc đợt 2 và chưa chia story. ⇒ Nội dung đã về đúng chỗ ở trang này; trang hướng dẫn chờ tới khi có màn thật.

E-001/1.4 Thu hồi EXECUTE hai hàm vùng nhật ký khỏi PUBLIC — admin-be

⟨CẦN NGƯỜI VIẾT — agent làn admin-be viết khi đóng change⟩

E-001/1.5 Cổng ma trận quyền fail-closed với tập cột rỗng — admin-be — admin-be

⟨CẦN NGƯỜI VIẾT — agent làn admin-be viết khi đóng change⟩

E-001/1.6 Hợp đồng CSDL của portal-be fail-closed với tập cột rỗng — portal-be

⟨CẦN NGƯỜI VIẾT — agent làn portal-be viết khi đóng change⟩