BI mã nguồn mở 7 — Phân quyền, bảo mật & RLS

13 thg 7, 2026 3 lượt xem
#security
#bi
#embedding
#rbac
#row-level-security

Vì sao bảo mật là bài toán khó nhất của BI ngân hàng

bài trước chúng ta đã dựng xong lớp semantic — metric và dataset dùng chung. Nhưng một nền BI đẹp về mặt mô hình vẫn là một quả bom nếu ai đăng nhập cũng thấy toàn bộ số dư và giao dịch của mọi khách hàng. Với NCB, cùng một dashboard "Cơ cấu huy động theo chi nhánh" sẽ được cả Hội sở và hàng chục chi nhánh mở ra — nhưng giám đốc chi nhánh Cần Thơ chỉ được thấy số của Cần Thơ, còn Hội sở thấy toàn hệ thống. Đây chính là bài toán trung tâm của bài này.

Bảo mật BI có nhiều tầng, và một sai sót ở bất kỳ tầng nào cũng khiến các tầng còn lại vô nghĩa:

  1. Xác thực (authentication) — bạn là ai? (LDAP/SSO/OAuth).
  2. Phân quyền chức năng (RBAC) — bạn được làm gì? (xem, tạo, sửa, xoá; truy cập database/dataset nào).
  3. Row-Level Security (RLS) — trong dataset được phép, bạn thấy những dòng nào?
  4. Column-Level Security — trong dòng đó, cột nào bị che (CCCD, số điện thoại)?
  5. Kết nối DB — Superset/Metabase nối vào warehouse bằng tài khoản gì, quyền tới đâu?
  6. Audit — ai đã xem/tải cái gì, lúc nào?

Mô hình phân quyền: RBAC

Superset — roles và permissions

Superset dùng Flask-AppBuilder (FAB) làm nền phân quyền, theo mô hình role-based access control (RBAC): mỗi người dùng có một hoặc nhiều role, mỗi role là một tập permission. Có sẵn vài role gốc quan trọng:

  • Admin — toàn quyền: quản lý người dùng, cấu hình database, thấy mọi thứ. Chỉ dành cho đội quản trị nền tảng.
  • Alpha — quyền của một "power user": tạo/sửa dataset, chart, dashboard, và thấy tất cả dữ liệu của mọi database đã kết nối, nhưng không quản trị được người dùng/cấu hình hệ thống.
  • Gamma — role "người tiêu thụ" (consumer). Điểm mấu chốt: Gamma không có sẵn quyền trên bất kỳ database/dataset nào. Bạn phải cấp quyền theo từng datasource cho role Gamma (hoặc tạo role tuỳ biến kế thừa Gamma). Đây là role nền cho hầu hết người dùng nghiệp vụ.
  • Public — quyền cho người chưa đăng nhập; mặc định gần như rỗng, không nên bật trừ khi cố ý làm dashboard công khai.
  • sql_lab — quyền dùng SQL Lab (IDE truy vấn); tách riêng để kiểm soát ai được viết SQL thô.

Cách làm chuẩn trong ngân hàng: không dùng thẳng Alpha/Gamma mà tạo các role tuỳ biến như Chi_nhanh_Viewer, DA_Team, Risk_Team, rồi gán permission ở mức từng dataset. Permission trong Superset có dạng rất chi tiết, ví dụ datasource access on [PostgreSQL_WH].[accounts_dataset] — nghĩa là role đó chỉ mở được đúng dataset đó. Không có permission tương ứng thì dataset và mọi chart dựa trên nó đều bị ẩn.

Metabase — groups, data permissions và collection permissions

Metabase không dùng khái niệm "role" mà dùng group (nhóm người dùng). Một người có thể thuộc nhiều group; quyền là hợp (union) — group cho phép rộng hơn sẽ thắng. Có hai group đặc biệt: Administrators (toàn quyền) và All Users (mọi người đều thuộc; thường cần hạ quyền group này xuống mức thấp nhất để tránh rò rỉ mặc định).

Metabase tách quyền thành hai trục độc lập:

  • Data permissions — group được truy cập database / schema / bảng nào, và ở mức nào: Can view (dùng qua giao diện query builder), No self-service, hay Granular. Có thêm quyền native query (được viết SQL thô hay không) — nên tắt cho phần lớn group nghiệp vụ.
  • Collection permissions — group thấy/sửa được các collection (thư mục chứa dashboard, câu hỏi) nào: Curate (sửa), View (chỉ xem), No access.

Sự tách đôi này rất hữu ích: bạn có thể cho một group View một collection dashboard nhưng vẫn No self-service trên database gốc — họ xem được dashboard mà không tự truy vấn lung tung được.

Khía cạnhSupersetMetabase
Đơn vị phân quyềnRole → permission chi tiếtGroup → data + collection permission
Đối tượng dữ liệuDatabase, schema, datasetDatabase, schema, table
Nhiều nhómUnion các roleUnion các group
Kiểm soát SQL thôRole sql_labQuyền native query per group
Tổ chức nội dungDashboard/chart + roleCollection + collection permission

Row-Level Security (RLS): mỗi chi nhánh chỉ thấy phần của mình

RBAC quyết định bạn thấy dataset nào; RLS quyết định bạn thấy dòng nào trong dataset đó. Đây là tính năng sống còn khi nhiều chi nhánh dùng chung một dashboard.

Superset: RLS filters gắn theo role

Superset có tính năng Row Level Security Filters (Settings → Row Level Security). Mỗi filter gồm: (1) áp lên dataset nào, (2) áp cho role nào, và (3) một biểu thức điều kiện SQL sẽ được Superset tự động chèn vào mệnh đề WHERE của mọi truy vấn sinh ra từ dataset đó.

Ví dụ: tạo filter trên dataset accounts_dataset, áp cho role CN_CanTho, với clause city = 'Can Tho'. Từ đó, bất kỳ chart nào một người thuộc role CN_CanTho mở ra đều bị Superset viết lại truy vấn thành ... WHERE (city = 'Can Tho'). Người dùng không nhìn thấy, không sửa được điều kiện này — nó nằm ở tầng server.

Superset phân biệt hai loại filter: Regular (nhiều filter cùng dataset áp cho một người sẽ nối bằng AND — càng nhiều filter càng thu hẹp) và Base (các filter Base nối bằng OR). Trong thực tế, mô hình phổ biến là mỗi role chi nhánh có một Regular filter lọc đúng chi nhánh của mình; ai không khớp role nào thì không có filter (Admin/Alpha thấy tất cả).

Một cách viết gọn hơn khi nhiều chi nhánh: thay vì hard-code 'Can Tho', dùng hàm ngữ cảnh người dùng trong clause (ví dụ so khớp thuộc tính người dùng với một bảng ánh xạ chi nhánh). Cách tiếp cận này giảm số filter phải bảo trì, nhưng cần đảm bảo bảng ánh xạ user→chi nhánh được kiểm soát chặt.

Metabase: data sandbox và impersonation

Metabase (bản Pro/Enterprise) giải bài toán tương tự bằng hai cơ chế:

  • Data sandbox — "đóng hộp cát" một bảng cho một group dựa trên một thuộc tính người dùng (user attribute). Bạn khai báo, ví dụ, mỗi tài khoản người dùng có attribute branch_city, rồi cấu hình sandbox trên bảng accounts: chỉ trả các dòng có city = {{branch_city}}. Có hai kiểu: basic sandbox (lọc theo cột trực tiếp) và advanced/custom sandbox (thay bảng gốc bằng một câu SQL đã lọc sẵn, còn dùng để ẩn cột — chỉ SELECT những cột được phép).
  • Connection impersonation — Metabase kết nối tới DB bằng một database role khớp với người dùng, rồi để chính DB thực thi RLS (ví dụ dùng Row Security Policy của PostgreSQL). Cách này đẩy việc thực thi bảo mật xuống tận CSDL — mạnh và khó lách, nhưng đòi hỏi quản lý role ở tầng database.
SupersetMetabase
Cơ chế RLSRLS Filter chèn WHERE theo roleData sandbox theo user attribute; hoặc impersonation
Nơi thực thiSuperset viết lại queryMetabase viết lại query, hoặc DB thực thi (impersonation)
Ẩn cộtCần cấu hình riêng ở datasetAdvanced sandbox có thể chọn cột
Bản cầnCó sẵn bản open-sourceData sandbox cần bản trả phí

Minh hoạ RLS bằng SQL chạy được

Sandbox học tập của chúng ta không có cột "chi nhánh" thật, nhưng ta mô phỏng chi nhánh bằng city (mỗi thành phố coi như một chi nhánh) và loại tiền bằng currency. Đây chính là hình dạng câu truy vấn mà Superset/Metabase tự sinh ra sau khi chèn điều kiện RLS — bạn viết chart bình thường, hệ thống thêm mệnh đề lọc.

Truy vấn dưới đây minh hoạ "chi nhánh Cần Thơ (city = 'Can Tho') chỉ thấy khách và số dư của mình" — điều kiện WHERE chính là thứ RLS filter tự động thêm:

-- ▶ Chạy được
SELECT c.city,
       a.currency,
       COUNT(DISTINCT c.id)  AS so_khach,
       COUNT(a.id)           AS so_tai_khoan,
       SUM(a.balance)        AS tong_so_du
FROM customers c
JOIN accounts a ON a.customer_id = c.id
WHERE c.city = 'Can Tho'
GROUP BY c.city, a.currency
ORDER BY tong_so_du DESC;

Nếu người dùng thuộc chi nhánh khác, RLS chỉ đổi hằng số trong WHERE (ví dụ c.city = 'Ha Noi') — cùng một dashboard, dữ liệu khác nhau tuỳ người xem. Ta cũng có thể mô phỏng một "chính sách chỉ xem ngoại tệ" (một số vai trò chỉ được thấy dòng tiền ngoại tệ) bằng cách RLS chèn thêm điều kiện lọc theo currency:

-- ▶ Chạy được
SELECT c.city,
       a.currency,
       SUM(a.balance)                              AS tong_so_du,
       ROUND(AVG(a.balance), 2)                    AS so_du_tb,
       MAX(t.created_at)                           AS gd_gan_nhat
FROM customers c
JOIN accounts a      ON a.customer_id = c.id
LEFT JOIN transactions t ON t.account_id = a.id
WHERE c.city = 'Can Tho'
  AND a.currency <> 'VND'
GROUP BY c.city, a.currency
ORDER BY tong_so_du DESC;

Điểm cốt lõi để ghi nhớ: điều kiện lọc RLS phải nằm ở tầng server (Superset/Metabase hoặc DB), không bao giờ ở phía trình duyệt. Nếu ta chỉ ẩn dòng bằng JavaScript hay filter trên dashboard, người dùng chỉnh URL/gọi API là lộ hết. RLS thật sự viết lại câu SQL trước khi nó chạm database.

Column-Level Security & che dữ liệu nhạy cảm

RLS lọc dòng; còn với cột nhạy cảm (CCCD, số điện thoại, địa chỉ, số tài khoản đầy đủ) ta cần column-level control hoặc masking:

  • Che ngay ở dataset/model: cách an toàn nhất là không expose cột thô lên lớp semantic. Thay vì đưa cột cccd vào dataset, tạo cột tính toán trả về dạng đã che (ví dụ chỉ hiện 4 số cuối). Trong Metabase advanced sandbox, bạn chọn đúng những cột được SELECT — cột không chọn thì group đó không truy cập được.
  • Masking tại view của warehouse: đẩy việc che xuống database bằng cách tạo view trả về cột đã mask, rồi chỉ cho BI kết nối vào view chứ không phải bảng gốc. Cách này bảo vệ cả những công cụ khác nối vào cùng DB, không riêng BI.
  • Data masking gốc DB: một số warehouse có dynamic data masking theo vai trò; nếu có, đây là tầng phòng thủ sâu nhất.

Nguyên tắc: quyết định "ai thấy cột gì" nên ở càng gần dữ liệu càng tốt, để mọi công cụ truy cập đều bị ràng buộc như nhau.

Xác thực & tích hợp doanh nghiệp: LDAP / SSO / OAuth

Trong ngân hàng, không ai muốn quản một danh sách tài khoản BI riêng. Cả hai công cụ đều tích hợp hệ định danh doanh nghiệp:

  • LDAP / Active Directory — người dùng đăng nhập bằng tài khoản AD của ngân hàng. Cả Superset (qua Flask-AppBuilder, AUTH_LDAP) và Metabase đều hỗ trợ. Có thể ánh xạ LDAP group → role/group BI để tự động phân quyền theo phòng ban.
  • SSO qua SAML / OpenID Connect (OIDC) / OAuth — tích hợp với Identity Provider (IdP) như Keycloak, Azure AD. Người dùng đăng nhập một lần, có MFA ở tầng IdP. Đây là lựa chọn khuyến nghị cho production.
  • Role/group mapping tự động — mục tiêu là JIT (just-in-time) provisioning: người mới vào phòng Rủi ro, được thêm vào AD group tương ứng, lần đầu đăng nhập BI là tự có đúng role. Giảm thao tác thủ công và tránh "role rác" tồn đọng khi nhân sự chuyển bộ phận.

Kết hợp SSO với phần quản lý vòng đời (khi nhân sự nghỉ, khoá AD là mất luôn quyền BI) là cách duy nhất giữ phân quyền không bị lệch theo thời gian. Xem thêm gov-06-access-control về mô hình kiểm soát truy cập chung.

Embedding an toàn

Ngân hàng thường muốn nhúng dashboard vào cổng nội bộ (portal tín dụng, app cán bộ). Nhúng sai cách là lỗ hổng lớn nhất, nên đây là phần cần cẩn thận nhất.

  • KHÔNG dùng public link / public embed cho dữ liệu khách hàng. Public URL nghĩa là ai có link đều xem được, không kiểm soát được — chỉ hợp cho dữ liệu thật sự công khai.
  • Signed embedding (embed có ký) — cách đúng. Ứng dụng backend của cổng tạo một token đã ký (JWT với secret dùng chung) chứa tham số lọc (ví dụ branch = 'Can Tho') và thời hạn hết hạn. Metabase có tính năng Signed embedding / static embedding làm đúng việc này; token quyết định người xem thấy phần dữ liệu nào, và ràng buộc lọc nằm trong token đã ký nên client không sửa được. Superset có SDK/Embedded để nhúng dashboard với guest token do server sinh, kèm RLS clause áp cho guest.
  • Không bao giờ lộ token phía client hay đưa thông tin kết nối DB ra frontend. Token phải sinh ở server ngay trước khi render; secret ký token giữ ở backend. Frontend chỉ nhận một iframe/URL đã ký, không thấy secret, không thấy chuỗi kết nối database.

Mấu chốt: với embedding, tham số bảo mật (chi nhánh, hạn dùng) đi trong token có chữ ký ở server, không phải query string mà người dùng chỉnh được.

Kết nối database an toàn

Đây là phần hay bị xem nhẹ nhưng cực kỳ quan trọng: BI nối vào warehouse bằng tài khoản nào?

  • Chỉ đọc (read-only) — tài khoản kết nối chỉ có quyền SELECT trên các schema/bảng/view cần thiết. BI không có lý do gì để INSERT/UPDATE/DELETE/DROP. Một tài khoản read-only chặn đứng cả tai nạn lẫn khai thác qua SQL Lab.
  • Không dùng superuser / tài khoản chung — tuyệt đối không nối bằng postgres hay tài khoản admin. Dùng một service account riêng cho BI, quyền tối thiểu.
  • Secret quản lý ngoài, không hardcode — chuỗi kết nối và mật khẩu không được nhét cứng trong code, docker-compose công khai, hay commit lên Git. Dùng biến môi trường + secret manager (Vault, Kubernetes Secret). Superset cho phép mã hoá chuỗi kết nối trong metadata bằng SECRET_KEY; hãy đặt secret key mạnh và bảo vệ nó.
  • Giới hạn phạm vi bằng view — cho service account BI chỉ thấy các view đã lọc/che, thay vì toàn bộ bảng gốc, để RLS/masking có thêm một lớp phòng thủ ở tầng DB.

Về vận hành bảo mật hạ tầng (TLS, network policy, quản lý secret), xem thêm devops-08-security-sre.

Audit: ai đã xem/tải gì

Tuân thủ đòi hỏi trả lời được câu "ai truy cập dữ liệu khách hàng nào, lúc nào". Nguồn audit:

  • Log truy vấn của Superset/Metabase — cả hai ghi lại hoạt động người dùng; Superset có bảng logs trong metadata DB ghi các sự kiện (mở dashboard, chạy query). Metabase có audit (bản trả phí) và view hoạt động.
  • Log ở tầng database — vì BI nối bằng một service account, log DB một mình không phân biệt được người dùng cuối; cần đối chiếu với log của BI để truy ra ai. Impersonation (Metabase) giúp log DB mang đúng danh tính.
  • Kiểm soát export — audit cả hành vi tải dữ liệu (export CSV/Excel), vì đó là lúc dữ liệu rời khỏi vùng kiểm soát.

Audit cần được chuyển sang hệ thống log tập trung (SIEM) và giữ đủ lâu theo quy định lưu vết. Đừng để log nằm trong chính metadata DB rồi bị xoay vòng mất.

Giới hạn export & watermark

Xuất dữ liệu là điểm rò rỉ cuối cùng — dữ liệu ra khỏi BI là hết kiểm soát kỹ thuật:

  • Tắt/giới hạn export cho các role không cần, hoặc giới hạn số dòng tối đa được tải.
  • Watermark — hiển thị tên người dùng + thời gian lên dashboard/bản in để nếu ảnh chụp màn hình bị rò, còn truy được nguồn.
  • Ưu tiên xem tổng hợp thay vì hàng chi tiết — với dữ liệu nhạy cảm, thiết kế dashboard ở mức tổng hợp (theo chi nhánh, theo tháng) hạn chế nhu cầu tải bảng chi tiết ra ngoài.

Việc hiển thị dữ liệu khách hàng còn ràng buộc bởi quy định bảo vệ dữ liệu cá nhân (ở Việt Nam là NĐ13/2023/NĐ-CP). Nguyên tắc tối thiểu hoá dữ liệu (chỉ hiện đúng phần cần cho nghiệp vụ) và giới hạn mục đích nên được nhúng thẳng vào cách thiết kế phân quyền và RLS ở trên. Xem gov-03-data-quality và các bài governance để nối vào khung tuân thủ chung.

Use case thực tế

Bối cảnh. NCB triển khai Superset dùng chung cho Hội sở và 12 chi nhánh. Dashboard "Cơ cấu huy động & dòng tiền" cần đến tay ~150 người, nhưng mỗi chi nhánh chỉ được thấy khách của mình; đội Rủi ro ở Hội sở thấy toàn hệ thống; cột CCCD/SĐT phải che.

Cách làm.

  1. Xác thực: bật SSO qua Keycloak (OIDC), MFA ở IdP. Mỗi tài khoản AD mang thuộc tính branch (mã chi nhánh).
  2. RBAC: tạo role tuỳ biến CN_Viewer (kế thừa Gamma) được cấp datasource access đúng dataset huy động; role Risk_HO được cấp thêm quyền rộng hơn. Không ai dùng Alpha/Admin trừ đội quản trị.
  3. RLS: tạo 12 RLS filter trên dataset huy động, mỗi filter áp cho một role chi nhánh với clause dạng city = '<chi nhánh>' (ở đây city mô phỏng chi nhánh). Role Risk_HO không có filter → thấy tất cả. Kết quả: cùng một dashboard, GĐ Cần Thơ mở ra chỉ thấy dòng city = 'Can Tho'.
  4. Column: dataset chỉ expose CCCD dạng che 4 số cuối (cột tính toán), không expose cột thô.
  5. DB: Superset nối warehouse bằng service account bi_ro chỉ có SELECT trên schema báo cáo; chuỗi kết nối để trong Kubernetes Secret, mã hoá bằng SECRET_KEY.
  6. Audit + export: bật ghi log truy cập, đẩy sang SIEM; role chi nhánh bị giới hạn export 10.000 dòng và có watermark tên người dùng.

Kết quả kiểm chứng. Đội bảo mật đăng nhập thử bằng tài khoản chi nhánh Cần Thơ, xác nhận truy vấn phát ra từ Superset (bắt ở log DB) đều mang WHERE city = 'Can Tho'; đổi URL dataset sang chi nhánh khác vẫn bị RLS chặn về đúng phần của mình. Một sự cố "rò dữ liệu qua public link" trước đây được đóng lại vì đã tắt public embed và chuyển sang guest token có ký.

Ghi nhớ

  • Bảo mật BI là nhiều tầng: xác thực → RBAC (dataset nào) → RLS (dòng nào) → column masking (cột nào) → kết nối DB → audit. Sai một tầng là hỏng cả chuỗi.
  • Superset: role Admin/Alpha/Gamma; Gamma không có sẵn quyền dữ liệu, phải cấp datasource access theo từng dataset. Nên tạo role tuỳ biến thay vì dùng thẳng Alpha.
  • Metabase: group + tách đôi data permissioncollection permission; hạ quyền group "All Users" xuống thấp nhất; tắt native query cho nhóm nghiệp vụ.
  • RLS: Superset chèn WHERE theo role (RLS filter); Metabase dùng data sandbox theo user attribute hoặc impersonation để DB tự thực thi. Điều kiện lọc luôn ở tầng server, không bao giờ ở client.
  • Che cột nhạy cảm càng gần dữ liệu càng tốt (view/mask ở warehouse) để mọi công cụ đều bị ràng buộc.
  • Embedding: dùng signed embed (token JWT ký ở server chứa tham số lọc + hạn dùng); không public link, không lộ token/chuỗi kết nối DB ra client.
  • Kết nối DB: service account read-only, không superuser, secret quản lý ngoài — không hardcode.
  • Audit & export: ghi ai xem/tải gì, đẩy sang SIEM; giới hạn export + watermark; thiết kế dashboard tổng hợp để giảm nhu cầu tải chi tiết. Tuân thủ NĐ13 bằng tối thiểu hoá dữ liệu.

Bài viết liên quan

Phân biệt ước lượng điểm và ước lượng khoảng, cách xây khoảng tin cậy (CI) bằng margin of error z*·SE / t*·SE, đánh đổi mức 90/95/99%, và cách DIỄN GIẢI ĐÚNG khoảng tin cậy (95% CI không phải xác suất tham số nằm trong khoảng). Có CI cho trung bình và cho tỷ lệ, ảnh hưởng của cỡ mẫu, cùng ví dụ ngân hàng về tỷ lệ nợ xấu và số dư trung bình.

13 thg 7, 2026 4

Kimball dimensional modeling: bảng fact/dimension, star vs snowflake, grain, và Slowly Changing Dimension.

13 thg 7, 2026 3

Khối OLAP, các thao tác drill-down/roll-up/slice & dice/pivot, OLAP vs OLTP và ROLAP/MOLAP/HOLAP.

13 thg 7, 2026 3

Từ nguồn dữ liệu qua ETL/ELT vào Data Warehouse, Data Mart đến dashboard; staging, ODS, batch vs streaming.

13 thg 7, 2026 3