G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
"""
|
|
라이선스 제한 강제 — FastAPI Dependency 함수 모음.
|
|
|
|
사용법:
|
|
from middleware.license_guard import check_institution_limit, check_server_limit, require_feature
|
|
|
|
@router.post("/")
|
|
async def create_inst(
|
|
payload: ...,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: None = Depends(check_institution_limit),
|
|
):
|
|
...
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import Depends, HTTPException
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database import get_db
|
|
from routers.license import get_license_status
|
|
|
|
|
|
async def _get_status(db: AsyncSession) -> dict:
|
|
return await get_license_status(db)
|
|
|
|
|
|
# ── 기관 수 제한 ───────────────────────────────────────────────────────────────
|
|
|
|
async def check_institution_limit(db: AsyncSession = Depends(get_db)) -> None:
|
|
"""기관 수가 라이선스 한도에 도달하면 HTTP 403."""
|
|
from models import Institution
|
|
status = await _get_status(db)
|
|
|
|
max_inst = (status.get("limits") or {}).get("max_institutions", 1)
|
|
if max_inst == -1:
|
|
return # 무제한
|
|
|
|
count_r = await db.execute(select(func.count()).select_from(Institution))
|
|
count = count_r.scalar_one()
|
|
|
|
if count >= max_inst:
|
|
edition = status.get("edition", "COMMUNITY")
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=(
|
|
f"라이선스 한도 초과: {edition} 에디션은 최대 {max_inst}개 기관까지 등록 가능합니다. "
|
|
f"현재 {count}개 등록됨. 라이선스를 업그레이드하세요."
|
|
),
|
|
)
|
|
|
|
|
|
# ── 서버 수 제한 ───────────────────────────────────────────────────────────────
|
|
|
|
async def check_server_limit(db: AsyncSession = Depends(get_db)) -> None:
|
|
"""서버 수가 라이선스 한도에 도달하면 HTTP 403."""
|
|
from models import Server
|
|
status = await _get_status(db)
|
|
|
|
max_srv = (status.get("limits") or {}).get("max_servers", 20)
|
|
if max_srv == -1:
|
|
return
|
|
|
|
count_r = await db.execute(select(func.count()).select_from(Server))
|
|
count = count_r.scalar_one()
|
|
|
|
if count >= max_srv:
|
|
edition = status.get("edition", "COMMUNITY")
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=(
|
|
f"라이선스 한도 초과: {edition} 에디션은 최대 {max_srv}개 서버까지 등록 가능합니다. "
|
|
f"현재 {count}개 등록됨. 라이선스를 업그레이드하세요."
|
|
),
|
|
)
|
|
|
|
|
|
# ── 사용자 수 제한 ─────────────────────────────────────────────────────────────
|
|
|
|
async def check_user_limit(db: AsyncSession = Depends(get_db)) -> None:
|
|
"""사용자 수가 라이선스 한도에 도달하면 HTTP 403."""
|
|
from models import User
|
|
status = await _get_status(db)
|
|
|
|
max_usr = (status.get("limits") or {}).get("max_users", 10)
|
|
if max_usr == -1:
|
|
return
|
|
|
|
count_r = await db.execute(select(func.count()).select_from(User))
|
|
count = count_r.scalar_one()
|
|
|
|
if count >= max_usr:
|
|
edition = status.get("edition", "COMMUNITY")
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=(
|
|
f"라이선스 한도 초과: {edition} 에디션은 최대 {max_usr}명 사용자까지 등록 가능합니다. "
|
|
f"현재 {count}명 등록됨. 라이선스를 업그레이드하세요."
|
|
),
|
|
)
|
|
|
|
|
|
# ── 기능 접근 제한 ─────────────────────────────────────────────────────────────
|
|
|
|
def require_feature(feature_name: str):
|
|
"""특정 기능이 현재 라이선스 에디션에 포함되어 있는지 검사하는 Dependency."""
|
|
async def _check(db: AsyncSession = Depends(get_db)) -> None:
|
|
status = await _get_status(db)
|
|
features: list = (status.get("limits") or {}).get("features", [])
|
|
if feature_name not in features:
|
|
edition = status.get("edition", "COMMUNITY")
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=(
|
|
f"'{feature_name}' 기능은 현재 라이선스({edition})에서 지원되지 않습니다. "
|
|
"라이선스를 업그레이드하세요."
|
|
),
|
|
)
|
|
return _check
|
|
|
|
|
|
# ── 현재 사용량 조회 ────────────────────────────────────────────────────────────
|
|
|
|
async def get_license_usage(db: AsyncSession = Depends(get_db)) -> dict:
|
|
"""현재 기관/사용자/서버 사용량 반환."""
|
|
from models import Institution, Server, User
|
|
|
|
inst_r = await db.execute(select(func.count()).select_from(Institution))
|
|
user_r = await db.execute(select(func.count()).select_from(User))
|
|
srv_r = await db.execute(select(func.count()).select_from(Server))
|
|
|
|
return {
|
|
"institutions": inst_r.scalar_one(),
|
|
"users": user_r.scalar_one(),
|
|
"servers": srv_r.scalar_one(),
|
|
}
|