perf: disable read-time sync and keep provider sync manual
This commit is contained in:
@@ -138,7 +138,6 @@ def list_companies(
|
|||||||
limit: int = Query(default=100, ge=1, le=500),
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
offset: int = Query(default=0, ge=0),
|
offset: int = Query(default=0, ge=0),
|
||||||
) -> ListResponse:
|
) -> ListResponse:
|
||||||
sync_from_provider(db)
|
|
||||||
repo = CompaniesRepository(db)
|
repo = CompaniesRepository(db)
|
||||||
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
|
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
|
||||||
return ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset)
|
return ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset)
|
||||||
@@ -231,7 +230,6 @@ def list_sites(
|
|||||||
limit: int = Query(default=100, ge=1, le=500),
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
offset: int = Query(default=0, ge=0),
|
offset: int = Query(default=0, ge=0),
|
||||||
) -> ListResponse:
|
) -> ListResponse:
|
||||||
sync_from_provider(db)
|
|
||||||
companies_repo = CompaniesRepository(db)
|
companies_repo = CompaniesRepository(db)
|
||||||
sites_repo = SitesRepository(db)
|
sites_repo = SitesRepository(db)
|
||||||
company_id = None
|
company_id = None
|
||||||
@@ -356,7 +354,6 @@ def list_systems(
|
|||||||
limit: int = Query(default=100, ge=1, le=500),
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
offset: int = Query(default=0, ge=0),
|
offset: int = Query(default=0, ge=0),
|
||||||
) -> ListResponse:
|
) -> ListResponse:
|
||||||
sync_from_provider(db)
|
|
||||||
repo = SystemsRepository(db)
|
repo = SystemsRepository(db)
|
||||||
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
|
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
|
||||||
return ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset)
|
return ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset)
|
||||||
@@ -386,7 +383,6 @@ def list_roles(
|
|||||||
limit: int = Query(default=100, ge=1, le=500),
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
offset: int = Query(default=0, ge=0),
|
offset: int = Query(default=0, ge=0),
|
||||||
) -> ListResponse:
|
) -> ListResponse:
|
||||||
sync_from_provider(db)
|
|
||||||
systems_repo = SystemsRepository(db)
|
systems_repo = SystemsRepository(db)
|
||||||
roles_repo = RolesRepository(db)
|
roles_repo = RolesRepository(db)
|
||||||
|
|
||||||
@@ -679,7 +675,6 @@ def list_members(
|
|||||||
limit: int = Query(default=100, ge=1, le=500),
|
limit: int = Query(default=100, ge=1, le=500),
|
||||||
offset: int = Query(default=0, ge=0),
|
offset: int = Query(default=0, ge=0),
|
||||||
) -> ListResponse:
|
) -> ListResponse:
|
||||||
sync_from_provider(db)
|
|
||||||
repo = UsersRepository(db)
|
repo = UsersRepository(db)
|
||||||
rows, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset)
|
rows, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset)
|
||||||
return ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset)
|
return ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ BUILTIN_CLIENT_IDS = {
|
|||||||
|
|
||||||
_sync_lock = threading.Lock()
|
_sync_lock = threading.Lock()
|
||||||
_last_synced_at = 0.0
|
_last_synced_at = 0.0
|
||||||
|
_last_systems_synced_at = 0.0
|
||||||
_min_sync_interval_sec = 30.0
|
_min_sync_interval_sec = 30.0
|
||||||
|
|
||||||
|
|
||||||
@@ -304,3 +305,102 @@ def sync_from_provider(db: Session, *, force: bool = False) -> dict[str, int]:
|
|||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
_sync_lock.release()
|
_sync_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_systems_from_provider(db: Session, *, force: bool = False) -> dict[str, int]:
|
||||||
|
global _last_systems_synced_at
|
||||||
|
now = time.time()
|
||||||
|
if not force and now - _last_systems_synced_at < _min_sync_interval_sec:
|
||||||
|
return {"synced": 0}
|
||||||
|
|
||||||
|
if not _sync_lock.acquire(blocking=False):
|
||||||
|
return {"synced": 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
now = time.time()
|
||||||
|
if not force and now - _last_systems_synced_at < _min_sync_interval_sec:
|
||||||
|
return {"synced": 0}
|
||||||
|
|
||||||
|
idp = ProviderAdminService(get_settings())
|
||||||
|
systems_repo = SystemsRepository(db)
|
||||||
|
roles_repo = RolesRepository(db)
|
||||||
|
|
||||||
|
systems_created = 0
|
||||||
|
systems_updated = 0
|
||||||
|
roles_created = 0
|
||||||
|
roles_updated = 0
|
||||||
|
|
||||||
|
client_rows = idp.list_clients()
|
||||||
|
for client in client_rows:
|
||||||
|
client_uuid = str(client.get("id", "")).strip()
|
||||||
|
client_id = str(client.get("clientId", "")).strip()
|
||||||
|
if not client_uuid or not client_id:
|
||||||
|
continue
|
||||||
|
if client_id in BUILTIN_CLIENT_IDS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
system = db.scalar(select(System).where(System.provider_client_id == client_id))
|
||||||
|
system_name = str(client.get("name", "")).strip() or client_id
|
||||||
|
system_status = "active" if client.get("enabled", True) else "inactive"
|
||||||
|
if system is None:
|
||||||
|
system_key = _generate_unique_key("SY", lambda key: systems_repo.get_by_key(key) is not None)
|
||||||
|
system = systems_repo.create(
|
||||||
|
system_key=system_key,
|
||||||
|
name=system_name,
|
||||||
|
provider_client_id=client_id,
|
||||||
|
status=system_status,
|
||||||
|
)
|
||||||
|
systems_created += 1
|
||||||
|
else:
|
||||||
|
system = systems_repo.update(
|
||||||
|
system,
|
||||||
|
name=system_name,
|
||||||
|
status=system_status,
|
||||||
|
)
|
||||||
|
systems_updated += 1
|
||||||
|
|
||||||
|
client_roles = idp.list_client_roles(client_uuid)
|
||||||
|
for role_row in client_roles:
|
||||||
|
if not isinstance(role_row, dict):
|
||||||
|
continue
|
||||||
|
role_name = str(role_row.get("name", "")).strip()
|
||||||
|
if not role_name:
|
||||||
|
continue
|
||||||
|
role_desc = str(role_row.get("description", "")).strip() or None
|
||||||
|
role_status = "active" if not role_row.get("composite", False) else "active"
|
||||||
|
role = db.scalar(
|
||||||
|
select(Role).where(
|
||||||
|
Role.system_id == system.id,
|
||||||
|
Role.provider_role_name == role_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if role is None:
|
||||||
|
role_key = _generate_unique_key("RL", lambda key: roles_repo.get_by_key(key) is not None)
|
||||||
|
roles_repo.create(
|
||||||
|
role_key=role_key,
|
||||||
|
system_id=system.id,
|
||||||
|
name=role_name,
|
||||||
|
description=role_desc,
|
||||||
|
provider_role_name=role_name,
|
||||||
|
status=role_status,
|
||||||
|
)
|
||||||
|
roles_created += 1
|
||||||
|
else:
|
||||||
|
roles_repo.update(
|
||||||
|
role,
|
||||||
|
name=role_name,
|
||||||
|
description=role_desc,
|
||||||
|
status=role_status,
|
||||||
|
)
|
||||||
|
roles_updated += 1
|
||||||
|
|
||||||
|
_last_systems_synced_at = time.time()
|
||||||
|
return {
|
||||||
|
"synced": 1,
|
||||||
|
"systems_created": systems_created,
|
||||||
|
"systems_updated": systems_updated,
|
||||||
|
"roles_created": roles_created,
|
||||||
|
"roles_updated": roles_updated,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
_sync_lock.release()
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
- 群組階層:`Company Group -> Site SubGroup`。
|
- 群組階層:`Company Group -> Site SubGroup`。
|
||||||
- 系統角色:以 Keycloak client role 表示,對應 DB `roles`。
|
- 系統角色:以 Keycloak client role 表示,對應 DB `roles`。
|
||||||
- `site_roles` 代表某 Site 擁有的 Keycloak role 集合。
|
- `site_roles` 代表某 Site 擁有的 Keycloak role 集合。
|
||||||
- 補齊策略:若 Keycloak 有、DB 沒有,後台同步流程會自動 upsert 到 DB。
|
- 同步策略改為手動觸發:不在列表讀取 (`R`) 時自動同步。
|
||||||
|
- 補齊策略:僅在手動同步按鈕(`POST /admin/sync/from-provider`)或 CUD 流程時同步。
|
||||||
- 使用者加入 Site 時,透過同步邏輯使其在 IdP 端取得對應角色能力。
|
- 使用者加入 Site 時,透過同步邏輯使其在 IdP 端取得對應角色能力。
|
||||||
|
|
||||||
## 後台安全線
|
## 後台安全線
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
- 欄位:`system_key`, `name`, `provider_client_id`, `status`
|
- 欄位:`system_key`, `name`, `provider_client_id`, `status`
|
||||||
- 系統詳情需顯示底下 `roles` 列表
|
- 系統詳情需顯示底下 `roles` 列表
|
||||||
- 建立/修改/刪除在 Keycloak 處理,member 後台提供「同步 Keycloak」按鈕
|
- 建立/修改/刪除在 Keycloak 處理,member 後台提供「同步 Keycloak」按鈕
|
||||||
|
- 所有資料列表頁不自動同步;需由使用者按下「同步」按鈕才觸發。
|
||||||
|
|
||||||
4. 角色管理(DB 關聯為主)
|
4. 角色管理(DB 關聯為主)
|
||||||
- 欄位:`role_key`, `system_key`, `name`, `description`, `provider_role_name`, `status`
|
- 欄位:`role_key`, `system_key`, `name`, `description`, `provider_role_name`, `status`
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ npm run dev
|
|||||||
3. `GET /me` 登入後應有資料。
|
3. `GET /me` 登入後應有資料。
|
||||||
4. 非 admin 群組帳號打 `/admin/*` 應為 403。
|
4. 非 admin 群組帳號打 `/admin/*` 應為 403。
|
||||||
5. `POST /admin/sync/from-provider?force=true` 可手動觸發全量補齊同步。
|
5. `POST /admin/sync/from-provider?force=true` 可手動觸發全量補齊同步。
|
||||||
|
6. 列表 API 不會自動同步 IdP(避免高負載),需手動按同步按鈕或呼叫同步 API。
|
||||||
|
|
||||||
## 6) 新模型驗收路徑
|
## 6) 新模型驗收路徑
|
||||||
1. 新增 Company、Site。
|
1. 新增 Company、Site。
|
||||||
|
|||||||
Reference in New Issue
Block a user