CYBERSOFT
Đăng nhập

Agent QA end-to-end cho SaaS multi-tenant: cô lập tenant, RBAC và billing

Tích hợpDịch vụ SaaSAI AgentSecurityThực tếPhỏng vấn
🗓 1 tháng trước22 phút đọc·👁 1,180 lượt xem👤 312 người đọc

Agent QA có AI kiểm end-to-end một SaaS multi-tenant: cô lập tenant làm oracle, ma trận RBAC, billing idempotent, thăm dò có guardrail, đối chiếu UI+API+dữ liệu, và các ca lỗi sâu như leo thang quyền và rò dữ liệu chéo.

1. Bối cảnh: SaaS multi-tenant và những bất biến sống còn

Một SaaS multi-tenant phục vụ nhiều khách hàng (tenant) trên cùng một hệ thống: Acme và Globex dùng chung code, chung database, nhưng dữ liệu của họ phải tuyệt đối tách biệt. Đây là mô hình kinh doanh phổ biến nhất của phần mềm hiện đại, và cũng là nơi một lỗi có thể phá huỷ cả công ty chỉ trong một sự cố. Nếu một người dùng của Acme nhìn thấy dù chỉ một hoá đơn của Globex, đó không phải 'bug nhỏ' — đó là vi phạm cô lập tenant (テナント分離), một sự cố bảo mật và pháp lý có thể chấm dứt hợp đồng và làm mất niềm tin không bao giờ lấy lại được. Bài này trình bày cách một agent QA end-to-end kiểm những bất biến đó bằng cách kết hợp UI, API và kiểm dữ liệu, trong khuôn khổ guardrail chặt chẽ.

Xuyên suốt bài, ta bám hai bất biến trụ cột. Thứ nhất là cô lập tenant: mọi truy vấn dữ liệu phải được lọc theo tenant_id, không có ngoại lệ, và không người dùng nào của tenant này thấy được dữ liệu của tenant khác. Thứ hai là đúng quyền theo RBAC: quyền thực tế của một người dùng không bao giờ vượt quá quyền mà role của họ cho phép, và cũng chỉ trong tenant của họ. Bên cạnh đó là bất biến về billing: đo lường (metering) đúng, tính tiền đúng, và mọi thao tác thanh toán phải idempotent để một lần bấm không bao giờ tính tiền hai lần. Ba nhóm bất biến này là oracle mà agent QA sẽ liên tục kiểm.

Bất biến cô lập tenant: không rò dữ liệu chéo (テナント分離) Tenant A (Acme) users: alice(admin), bob(viewer) invoices: INV-A-001 … A-042 data scope = tenant_id = A RBAC: admin ⊃ editor ⊃ viewer Tenant B (Globex) users: carol(admin), dan(editor) invoices: INV-B-001 … B-118 data scope = tenant_id = B RBAC: admin ⊃ editor ⊃ viewer TƯỜNG CÔ LẬP — bob KHÔNG BAO GIỜ thấy INV-B-* Oracle #1: mọi truy vấn LỌC theo tenant_id — không có ngoại lệ Oracle #2: RBAC — quyền của user ≤ quyền role trong tenant CỦA HỌ Rò 1 dòng dữ liệu chéo tenant = sự cố nghiêm trọng, không phải "bug nhỏ"
Bất biến cô lập tenant và RBAC: tường ngăn giữa Tenant A và B — không rò dữ liệu chéo.
Trong multi-tenant, phần lớn lỗi nghiêm trọng không phải 'tính năng không chạy' mà là 'tính năng chạy quá tay': trả về nhiều hơn được phép, cho quyền cao hơn role. Oracle phải kiểm cả điều KHÔNG được xảy ra, không chỉ điều nên xảy ra.

2. Cô lập tenant làm oracle: bất biến 'không rò dữ liệu chéo'

Cô lập tenant là oracle mạnh nhất và cũng khó kiểm đầy đủ nhất, vì nó là một bất biến phủ định: không được có bất kỳ đường nào để dữ liệu của tenant này lọt sang tenant khác. Không thể liệt kê hết mọi đường, nên chiến lược là kiểm theo lớp. Ở tầng dữ liệu, ta khẳng định mọi truy vấn đều mang điều kiện tenant_id. Ở tầng API, ta thử đăng nhập bằng tài khoản tenant A rồi cố tình yêu cầu định danh tài nguyên của tenant B, và oracle là hệ thống phải trả 403 hoặc 404 — tuyệt đối không phải 200 kèm dữ liệu. Ở tầng UI, ta kiểm rằng không màn hình nào, không bộ lọc nào, không đường dẫn sâu nào để lộ dữ liệu chéo. Agent QA đi qua cả ba lớp cho cùng một bất biến.

ts
// tests/isolation/cross-tenant.spec.ts — oracle PHỦ ĐỊNH: không rò chéo
import { test, expect } from '@playwright/test';
import { db } from '../support/db';

test('user tenant A KHÔNG truy cập được tài nguyên tenant B', async ({ request }) => {
  const aToken = await loginAs('alice@acme.test');       // tenant A
  const bInvoice = await db.oneInvoiceOf('tenant-B');    // 1 hoá đơn thật của B

  // Cố tình gọi bằng token A, xin resource của B
  const res = await request.get(`/api/invoices/${bInvoice.id}`,
    { headers: { Authorization: `Bearer ${aToken}` } });

  // Oracle: PHẢI bị chặn. 200 kèm data = sự cố nghiêm trọng.
  expect([403, 404]).toContain(res.status());
  if (res.status() === 200) {
    throw new Error('TENANT LEAK: A đọc được hoá đơn của B — sự cố nghiêm trọng');
  }
});

Chú ý test trên dùng 404 hoặc 403 làm oracle, và có lý do tinh tế. Trả 403 (cấm) tiết lộ rằng tài nguyên tồn tại nhưng bạn không có quyền — đôi khi đây đã là rò rỉ thông tin. Nhiều hệ thống multi-tenant cẩn thận chọn trả 404 (không tồn tại) cho tài nguyên của tenant khác, để người dùng tenant A thậm chí không biết một hoá đơn của B có tồn tại hay không. Việc chọn 403 hay 404 là một quyết định thiết kế bảo mật mà QA cần biết để đặt oracle đúng. Điều tuyệt đối cấm là 200 kèm dữ liệu: đó là rò rỉ thật sự, và test phải hét lên rõ ràng khi gặp.

⚠️ Đừng chỉ kiểm cô lập ở tầng UI. UI có thể ẩn nút nhưng API vẫn trả dữ liệu nếu gọi trực tiếp. Kẻ tấn công không dùng UI của bạn — họ gọi thẳng API. Oracle cô lập PHẢI được kiểm ở tầng API và tầng dữ liệu, không chỉ UI.

3. Ma trận RBAC: kiểm quyền theo role × hành động × tenant

RBAC (kiểm soát truy cập theo vai trò) trong multi-tenant là một không gian tổ hợp: mỗi role (admin, editor, viewer) có một tập hành động được phép, và điều đó phải đúng trong tenant của người dùng và sai ngoài tenant đó. Cách kiểm hiệu quả là dựng một ma trận: hàng là role, cột là hành động, ô là kết quả kỳ vọng (cho phép hay từ chối). Ma trận này chính là oracle — nó phát biểu rõ ràng ai được làm gì. Điều quan trọng là ma trận phải kiểm cả hai chiều: viewer phải bị từ chối khi cố xoá (quyền không được vượt role), và admin phải được phép khi cần (quyền không được thiếu). Nhiều bug bảo mật đến từ việc chỉ kiểm 'admin làm được' mà quên kiểm 'viewer không làm được'.

ts
// tests/rbac/matrix.spec.ts — ma trận role × action, kiểm CẢ HAI chiều
const MATRIX: Record<string, Record<string, boolean>> = {
  admin:  { read: true,  create: true,  update: true,  delete: true,  billing: true },
  editor: { read: true,  create: true,  update: true,  delete: false, billing: false },
  viewer: { read: true,  create: false, update: false, delete: false, billing: false },
};

for (const [role, actions] of Object.entries(MATRIX)) {
  for (const [action, allowed] of Object.entries(actions)) {
    test(`${role} ${action} -> ${allowed ? 'ALLOW' : 'DENY'}`, async ({ request }) => {
      const token = await loginWithRole(role, 'tenant-A');
      const res = await callAction(request, action, token);
      if (allowed) expect(res.status()).toBeLessThan(300);       // được phép
      else expect([403, 404]).toContain(res.status());           // bị từ chối
    });
  }
}

Sinh test từ một ma trận có hai lợi ích lớn. Thứ nhất, nó đảm bảo tính đầy đủ: mọi ô role × action đều được kiểm, không sót trường hợp 'viewer xoá'. Thứ hai, bản thân ma trận trở thành tài liệu sống về chính sách phân quyền — khi ai đó muốn biết editor có được xoá không, họ đọc ma trận chứ không phải đọc code. Đây là nơi AI hữu ích: bạn mô tả chính sách RBAC bằng ngôn ngữ tự nhiên, AI dựng ma trận đầu tiên, rồi bạn — con người — review từng ô, đặc biệt các ô 'từ chối', vì đó là nơi bug bảo mật ẩn nấp. AI dựng khung nhanh; con người xác nhận đúng chính sách nghiệp vụ.

Vì sao kiểm 'viewer KHÔNG xoá được' lại quan trọng ngang 'admin xoá được'?

Vì lỗ hổng leo thang quyền (privilege escalation) nằm ở chiều phủ định. Nếu chỉ kiểm 'admin xoá được', một bug cho phép viewer cũng xoá được sẽ lọt qua hoàn toàn — pipeline vẫn xanh vì admin vẫn hoạt động. Chiều 'ai KHÔNG được làm gì' mới là nơi bảo mật thật sự sống. Một hệ thống mà mọi role đều làm được mọi thứ vẫn 'chạy đúng' theo nghĩa chức năng, nhưng là thảm hoạ bảo mật. Vì thế ma trận RBAC phải cân bằng: mỗi ô 'cho phép' đi kèm các ô 'từ chối' tương ứng, và các ô từ chối thường là nơi tôi soi kỹ nhất.

4. Billing và metering: đúng số, đúng lần, idempotent

Billing trong SaaS là nơi lỗi biến trực tiếp thành tiền mất hoặc khách giận. Có ba nhóm bất biến. Thứ nhất là metering đúng: nếu tính tiền theo lượng dùng (số API call, số user, dung lượng), thì con số đo phải khớp thực tế — không tính thiếu (mất doanh thu) cũng không tính thừa (mất niềm tin). Thứ hai là tính tiền đúng: áp đúng gói, đúng đơn giá, đúng giảm giá, đúng thuế. Thứ ba, và thường bị bỏ sót nhất, là idempotency của thao tác thanh toán: một webhook thanh toán bị gửi lại, một người dùng bấm 'thanh toán' hai lần vì mạng chậm — hệ thống tuyệt đối không được tính tiền hai lần. Đây chính là bất biến idempotency (冪等性) trong ngữ cảnh billing.

ts
// tests/billing/idempotency.spec.ts — webhook gửi lại KHÔNG tính tiền 2 lần
import { test, expect } from '@playwright/test';

test('webhook thanh toán trùng eventId -> chỉ ghi nợ MỘT lần', async ({ request }) => {
  const eventId = 'evt_' + crypto.randomUUID();
  const payload = { eventId, tenantId: 'tenant-A', amount: 4900, type: 'invoice.paid' };

  // Gửi cùng một webhook 3 lần (mô phỏng retry của cổng thanh toán)
  for (let i = 0; i < 3; i++) {
    await request.post('/webhooks/billing', { data: payload,
      headers: { 'x-signature': sign(payload) } });
  }

  // Oracle: chỉ đúng 1 bản ghi thanh toán cho eventId này
  const charges = await request.get(`/api/charges?eventId=${eventId}`);
  expect((await charges.json()).length).toBe(1);
  // Oracle 2: số dư tenant chỉ bị trừ 4900 MỘT lần
  const acct = await request.get('/api/tenants/tenant-A/account');
  expect((await acct.json()).lastCharge).toBe(4900);
});
🔒

Nội dung bị khoá

Bạn đang xem bản xem trước. Đăng nhập để mở khoá toàn bộ bài viết và tất cả tài liệu Tester/QA.

Đã xem 24% nội dung
  • Đọc trọn vẹn mọi bài viết
  • Lưu bài & ghi chú cá nhân
  • Theo dõi tiến độ đã đọc
  • Luyện phỏng vấn, ISTQB, Mock
Đăng nhập để đọc tiếp

Chưa có mã? Lấy mã qua Fanpage / Zalo CyberSoft.

0

💬 Bình luận (0)

Bạn có thể đọc mọi bình luận. Đăng nhập để bình luận

Chưa có bình luận nào. Hãy là người đầu tiên!