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(...) và 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-006 và
FR-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.3và 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/DELETEvẫn không chặnDROPvàTRUNCATEcủa chủ sở hữu bảng — màAD-14vừa giaoadmin-becầm danh tính DDL. Một danh tính cầm cả hai vùng là mở đúng cửa sau màNFR-006tuyê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 CASCADExoá sạch vết theo dòng cha trong khiREVOKE DELETEvẫ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-004–E-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
DELETEtrê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ộcAD-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ênadmin-betự nghĩ — trong khi kiến trúc (AD-20) và tiêu chí của story đều khaiadmin_app.AD-15mô 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ềnALL, 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
INSERTmà cổng khởi động không thấy;portal-betừ 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 đồngportal-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-006vàFR-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.1 và 1.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.md và spec.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ông có DROP/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ênapp, nhưng trênauditchỉSELECTvàINSERT. Ứng dụng không bao giờ kết nối bằng vai Flyway. - ⚠️ Bẫy đã ghi tên:
ALTER DEFAULT PRIVILEGESchỉ áp cho đối tượng do vai nêu trongFOR ROLEtạ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 choflyway_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-migratecầ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=1lặng lẽ trả trang thứ hai — sai mà không ai thấy.AD-28đòi ca lướipage=0bị 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ích —
E-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 repoportal-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.2 và 1.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 PRIVILEGESchỉ áp cho vai nêu trongFOR ROLE, vàCREATE SCHEMA IF NOT EXISTSbỏ quaAUTHORIZATIONkhi 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
GRANTmà vẫn đểREVOKEchạ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í:
| Mã | 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. Change2026-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àndocsquyết sau khi đọc hồ sơ; PO duyệt 07/09/2026.⚠️ Khoá
docs:tronglink.yamlcủ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 docsmụ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 6ac16cb — hai 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ó và 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_runtime → admin_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 và 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) và 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_runtime ⛔ khô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
PRIVILEGES là trạ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 ở README và security.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ẫn có
INSERT. «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 đã đóng ở admin-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 |
| Có 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á¶
- ⛔ 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.
- ⚠️ 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».
- ⚠️ 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.1chư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⟩