4 phút đọc

Quyền có trong bảng phân quyền nhưng không route nào kiểm tra

Bảng phân quyền ghi session.manage chỉ dành cho quản trị hệ thống, còn route thu hồi phiên đăng nhập lại kiểm một quyền khác. Test quét route vẫn xanh. Cách phát hiện quyền khai báo mà không ai dùng, và một test ngược chiều để bắt nó.

Một hệ thống tuyển sinh có 50 quyền và 5 vai trò (cán bộ, trưởng ban, biên tập, quản trị hệ thống, người xem). Mỗi quyền là một chuỗi như application.review hay user.manage, và có một bảng ghi vai trò nào được quyền nào. Bảng này còn có test riêng, viết tay độc lập với code, để ai sửa nhầm một ô là test đỏ.

Robot linh vật đứng trước bảng điều khiển nhiều công tắc, một sợi dây không nối vào đâu

Vậy mà vẫn có một quyền nằm trong bảng, đúng từng ô, và không route nào hỏi tới nó.

Cái gì đã xảy ra

Quyền session.manage nghĩa là “được thu hồi phiên đăng nhập của người khác”. Trong bảng, chỉ quản trị hệ thống có quyền này. Còn route làm việc đó, DELETE /api/admin/users/[id]/sessions, lại khai báo:

export const DELETE = handler(
  {
    auth: 'staff',
    permission: 'user.manage',   // bảng nói session.manage
    // ...

Sửa là đổi đúng một chuỗi. Với bảng hiện tại, user.manage và session.manage cùng chỉ có ở quản trị hệ thống, nên chưa ai bị ảnh hưởng. Nhưng lỗi vẫn thật: bảng phân quyền nói một đằng, code làm một nẻo. Ngày bạn muốn cho trưởng ban thu hồi phiên mà không được sửa tài khoản, bạn cấp session.manage trong bảng, test bảng xanh, và trưởng ban vẫn bị chặn. Ngược lại, ai đó được cấp user.manage để quản lý tài khoản sẽ tự nhiên có thêm quyền ngắt phiên của mọi người.

Quyền được cấp nhiều hơn dự định thì là lỗ hổng. Được cấp ít hơn thì là lỗi khó hiểu. Cả hai đến từ cùng một chỗ.

Vì sao hai lớp test đều không thấy

Dự án này đã làm khá kỹ:

  • Test ma trận: mỗi quyền, mỗi vai trò, có hay không, khớp với một bảng viết tay.
  • Test E2E quét mã nguồn, liệt kê mọi route, và bắt buộc route nào cho nhân viên đều phải khai báo một quyền. Ai quên khai báo là đỏ.

Hai test này cùng nhìn một hướng: từ route đi tới quyền (“route có khai báo quyền không”) và từ vai trò đi tới quyền (“vai trò có đúng quyền không”). Không test nào hỏi chiều ngược lại: quyền này có route nào dùng không? Route khai báo user.manage là hợp lệ. Quyền session.manage có mặt trong bảng là hợp lệ. Mỗi mảnh đều đúng, chỉ cái mối nối giữa chúng đứt.

Mình thấy kiểu đứt này hay gặp ở code do AI viết từng mảnh. Một lần nhờ “thêm quyền session.manage vào bảng”, lần khác nhờ “viết route thu hồi phiên”. Mỗi lần AI làm đúng việc được giao, và chọn quyền gần nghĩa nhất đang có sẵn ở lần sau.

Test ngược chiều

Ý rất ngắn: mọi quyền trong danh sách phải được dùng ít nhất một lần ngoài file định nghĩa.

import { PERMISSIONS } from './permissions'

it('mọi quyền đều có nơi kiểm tra', () => {
  const source = readAllSources(['apps', 'packages'], { skip: ['auth/permissions'] })
  for (const p of PERMISSIONS) {
    expect(source, `quyền ${p} không nơi nào dùng`).toContain(`'${p}'`)
  }
})

Còn nếu chưa muốn viết test, quét tay bằng grep cũng được: với mỗi quyền, tìm chuỗi 'tên.quyền' trong code, bỏ file định nghĩa và file test ma trận. Không ra kết quả nào là quyền đang treo.

Mình chạy cách quét này trên dự án sau khi sửa: cả 50 quyền đều có chỗ dùng. Trước khi sửa, session.manage là quyền duy nhất không route nào gọi tên.

Cách này chỉ bắt được “quyền không ai dùng”. Nó không bắt được trường hợp route dùng nhầm một quyền khác đã có người dùng. Cái đó cần đọc từng route đối chiếu với mô tả quyền, hoặc viết test E2E gọi route bằng từng vai trò và xem ai bị chặn. Dự án này có test kiểu đó cho các route nhạy cảm.

Kiểm tra app của bạn

  1. Liệt kê toàn bộ quyền hoặc vai trò của app (enum, bảng, file cấu hình).
  2. Với mỗi quyền, grep xem có bao nhiêu route hoặc hàm kiểm tra nó. Quyền ra 0 kết quả thì hoặc là thừa, hoặc là một chỗ nào đó đang kiểm nhầm quyền khác.
  3. Đọc lại mô tả của mỗi quyền và mở route tương ứng, xem hành động thực tế có đúng như tên quyền nói không. Chú ý các hành động cắt đứt hay xóa: thu hồi phiên, xóa tài khoản, xuất dữ liệu.
  4. Thêm test ngược chiều ở trên để lần sau AI thêm quyền mà quên nối, test đỏ ngay.
Chia sẻ lên Facebook

Bình luận

Đang tải bình luận…