Migration role flags sang policy flags
Scope: thiết kế migration backward-compatible cho
roles.admin_access/app_accesssang policy-leveladmin_access/app_access/enforce_tfa/ip_allow/ip_deny/valid_from/valid_until.
1. Trạng thái hiện tại
- Schema
policiesđã có các cộtadmin_access,app_access,enforce_tfa,ip_allow,ip_deny,valid_from,valid_until. - Schema
rolesvẫn cònadmin_accessvàapp_accessđể tương thích. PermissionServicehiện tính effective access theo kiểu compatibility: legacy role flags OR active policy flags.- Migration trước đây (
0010_loud_the_renegades.sql, nay đã gộp vào0000_lumibase_init) đã thêm policy flags nhưng chưa materialize role flags hiện có thành policy rows.
Kết luận: có thể migrate an toàn nếu không xoá hoặc tắt legacy role flags ngay. Policy flags trở thành source of truth mới, còn role flags là fallback trong một release window.
2. Mục tiêu
- Không làm mất quyền admin/app hiện tại của bất kỳ role/user/site nào.
- Đưa
adminAccess,appAccess,enforceTfa, IP guard và time window về policy level. - Giữ role là grouping/assignment unit, không phải nơi chứa security guard.
- Chuẩn bị cho seed
policy_admin,policy_studio_self, import/exportlumibase.access@v1, app access enforcement và TFA enforcement. - Cho phép rollback không mất quyền.
3. Migration phases
Phase A: compatibility window
Trạng thái này đang đúng với implementation hiện tại.
- Giữ
roles.admin_access/app_access. - Compile effective flags bằng
role flags OR active policy flags. - Studio đánh dấu role-level toggles là deprecated/read-only hoặc hiển thị cảnh báo.
- Policy Detail là nơi chỉnh flags mới.
- Import/export ưu tiên policy flags; role flags chỉ export dưới dạng legacy metadata nếu thật sự cần debug.
Phase B: materialize legacy role flags
Tạo một policy legacy cho mỗi role đang có admin_access=true hoặc app_access=true.
Contract:
- Key policy:
legacy_role_flags_<stable_role_key>. - Name:
Legacy role flags: <role name>. admin_access= giá trị hiện tại của role.app_access= giá trị hiện tại của role.enforce_tfa=false.ip_allow=[],ip_deny=[].valid_from=null,valid_until=null.- Không tạo permission rows cho policy legacy, vì policy này chỉ mang access flags.
- Attach policy legacy vào chính role qua
role_policies. - Không mutate role flags ở phase này.
Lý do giữ đúng từng flag thay vì suy luận admin_access => app_access: dữ liệu legacy có thể tồn tại role admin API-only hoặc role app-only. Migration phải preserve exact effective behavior trước khi áp chính sách mới.
Phase C: chuyển UI và seed sang policy-first
- Role Detail không còn là nơi chỉnh
adminAccess/appAccess; thay vào đó có quick action tạo/attach policy phù hợp. - Policy Detail sở hữu
adminAccess,appAccess,enforceTfa,ipAllow,ipDeny,validFrom,validUntil. - Seed mới không set role flags, trừ khi cần compatibility cho version cũ.
- Seed tối thiểu:
role_administratorpolicy_adminvớiadminAccess=true,appAccess=true,enforceTfa=true- attach
policy_adminvàorole_administrator policy_studio_selfvớiappAccess=truevà quyền self-service tối thiểupolicy_publicvớiadminAccess=false,appAccess=false
Phase D: shadow verify và tắt fallback
Trước khi tắt role fallback, chạy report so sánh:
- effective admin/app từ legacy role flags
- effective admin/app từ attached active policies
- danh sách role còn flag legacy true nhưng không có policy tương ứng
Chỉ sau khi report sạch mới bật cấu hình:
LUMIBASE_RBAC_LEGACY_ROLE_FLAGS=false
Khi flag này false, PermissionService chỉ dùng policy flags. Cột role flags vẫn nên giữ thêm ít nhất một release để rollback.
Phase E: drop legacy columns
Chỉ thực hiện sau một release boundary rõ ràng:
- không còn code đọc role flags
- không còn Studio control ghi role flags
- export/import không cần legacy metadata
- migration verification đã chạy sạch trên production
4. SQL strategy idempotent
Đây là pseudo-SQL, không phải migration cuối cùng. Implementation nên dùng Drizzle/SQL transaction và nanoid generator nhất quán với project.
WITH legacy_roles AS (
SELECT
id AS role_id,
site_id,
name,
admin_access,
app_access,
lower(
regexp_replace(
coalesce(system_key, key, id),
'[^a-zA-Z0-9]+',
'_',
'g'
)
) AS role_key
FROM lumibase_roles
WHERE admin_access = true OR app_access = true
),
upserted_policies AS (
INSERT INTO lumibase_policies (
id,
site_id,
key,
name,
description,
admin_access,
app_access,
enforce_tfa,
ip_allow,
ip_deny,
valid_from,
valid_until,
rules
)
SELECT
gen_lumibase_id(),
site_id,
'legacy_role_flags_' || role_key,
'Legacy role flags: ' || name,
'Backfilled from roles.admin_access/app_access. Do not edit manually after policy migration is complete.',
admin_access,
app_access,
false,
'[]'::jsonb,
'[]'::jsonb,
null,
null,
'{}'::jsonb
FROM legacy_roles
ON CONFLICT (site_id, key) DO UPDATE SET
admin_access = excluded.admin_access,
app_access = excluded.app_access,
enforce_tfa = excluded.enforce_tfa,
ip_allow = excluded.ip_allow,
ip_deny = excluded.ip_deny,
valid_from = excluded.valid_from,
valid_until = excluded.valid_until
RETURNING id, site_id, key
)
INSERT INTO lumibase_role_policies (role_id, policy_id, priority)
SELECT lr.role_id, p.id, 5
FROM legacy_roles lr
JOIN upserted_policies p
ON p.site_id = lr.site_id
AND p.key = 'legacy_role_flags_' || lr.role_key
ON CONFLICT DO NOTHING;
Implementation notes:
- Nếu DB không có
gen_lumibase_id(), tạo ID trong application migration. priority=5chỉ là convention. Policy flag không có permission rows nên không ảnh hưởng merge permission rows.ON CONFLICT (site_id, key)yêu cầu unique indexpolicies_site_key_unique.
5. Verification checklist
Pre-check:
SELECT site_id, count(*) AS role_count
FROM lumibase_roles
WHERE admin_access = true OR app_access = true
GROUP BY site_id;
Post-check:
SELECT r.id, r.site_id, r.name, r.admin_access, r.app_access
FROM lumibase_roles r
WHERE (r.admin_access = true OR r.app_access = true)
AND NOT EXISTS (
SELECT 1
FROM lumibase_role_policies rp
JOIN lumibase_policies p ON p.id = rp.policy_id
WHERE rp.role_id = r.id
AND p.admin_access = r.admin_access
AND p.app_access = r.app_access
);
Expected result: zero rows.
Behavior tests:
- Role admin legacy trước migration vẫn admin sau migration.
- Role app-only legacy trước migration vẫn vào Studio sau migration.
- Role không có app access vẫn không vào Studio.
- Role không có admin access vẫn không bypass permission rows.
- Tắt
LUMIBASE_RBAC_LEGACY_ROLE_FLAGStrên fixture đã backfill không đổi effective result. - Rollback xoá policy key prefix
legacy_role_flags_vẫn không mất quyền vì role flags còn nguyên.
6. Rollback
Rollback an toàn trong compatibility window:
DELETE FROM lumibase_role_policies
WHERE policy_id IN (
SELECT id FROM lumibase_policies WHERE key LIKE 'legacy_role_flags_%'
);
DELETE FROM lumibase_policies
WHERE key LIKE 'legacy_role_flags_%';
Không xoá hoặc reset roles.admin_access/app_access trong cùng migration. Đây là điểm bảo vệ backward-compatible quan trọng nhất.
7. Ảnh hưởng tới feature tiếp theo
App access enforcement:
- Studio phải check
effective.appAccess=truetừ active policies. - Trong compatibility window, role flags vẫn OR vào kết quả để không phá user hiện tại.
- API key luôn bị chặn khỏi Studio, kể cả khi policy có
appAccess=true.
TFA enforcement:
- Backfill legacy policy đặt
enforceTfa=falsevì role flags cũ không có nghĩa tương đương. policy_adminseed mới nên đặtenforceTfa=true.- API key attach vào policy có
enforceTfa=truephải bị blocking conflict hoặc warning mạnh, vì API key không enroll TFA.
IP/time guards:
- Backfill legacy policy để
ipAllow=[],ipDeny=[],validFrom=null,validUntil=null. - Các guard mới chỉ được thêm qua Policy Detail, seed hoặc import manifest.
Import/export:
lumibase.access@v1export policy flags là source of truth.- Legacy role flags không được dùng để tạo quyền mới trong môi trường đích.
- Nếu cần trace migration, export legacy role flags trong
metadata.legacyRoleFlags, không trong contract chính.
Studio deprecation:
- Role page chỉ hiển thị legacy flags dạng read-only trong migration window.
- Save role không nên ghi
adminAccess/appAccess. - Nút sửa access flag nên điều hướng sang Policy Detail hoặc tạo policy mới rồi attach.
System seeding:
- New seed dùng
policy_admin,policy_studio_self,policy_public. - Không seed role-level access flags cho môi trường mới, trừ khi đang hỗ trợ version cũ chưa đọc policy flags.