824a3100ce
이번 PR 은 invyone 멀티테넌시 SaaS 의 "관리 plane vs 테넌트 plane" 격리를 4 영역(PR #A~D) 에서 강화하고, 별도로 진행 중이던 부서관리 후속 작업을 포함한다. # 보안 (plane 격리) PR #A — controller/CompanyManagementController 인증 누락 패치 /api/company-management/* 가 JWT/role/host 체크 없이 외부에서 누구나 회사 삭제 + 디스크 통계 호출 가능했던 critical 누수 막음. SuperAdminGuard.enforce() 적용. PR #C — cross-tenant 컨트롤러 호스트 격리 + 감사 로그 CrossTenantContext.requireManagementHost() 헬퍼 추가, 5 컨트롤러 (CrossTenantContext/Controller/UserController/RoleController/DeptController) 모두 테넌트 호스트에서 호출 시 403. CompanyAuditLogService 에 cross-tenant write 4종 (USER_CREATE/DELETE, PW_RESET, ROLE_UPDATE) audit action 추가. SuperAdminGuard.isTenantHost 가시성 public static 으로 승격. PR #B — 프론트 솔루션 전용 admin 페이지 가드 admin/* 페이지 전수 분류 결과 솔루션 전용 3건 식별: subdomainList / companyList / audit-log. 각 페이지에 isManagementHost useEffect 가드 + redirect 추가. 사이드바도 같이 숨김. PR #D — MENU_INFO.IS_SOLUTION_ONLY 컬럼 + DB-driven 메뉴 필터 V023 마이그레이션으로 컬럼 추가 + 솔루션 메뉴 3개 마킹. admin.xml selectUserMenuList 에 호스트 기반 필터 추가, AdminController.getUserMenus 가 Host 헤더로 is_management_host 결정. 프론트 MANAGEMENT_ONLY_MENU_URLS 하드코딩 set 폐기 (DB 가 대신함). 페이지 자체 가드는 defense in depth 로 유지. StartupSchemaMigrator 에 V023 등록되어 모든 테넌트 DB 부팅 시 자동 적용. # 부서관리 후속 (이전 PR #18/#19 follow-up) DepartmentController/Service + frontend deptMngList/department.ts 의 추가 작업분. 이번 격리 작업과 무관하지만 같이 정리. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
2.5 KiB
Java
60 lines
2.5 KiB
Java
package com.erp.crosstenant;
|
|
|
|
import com.erp.tenant.DbContextHolder;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.web.server.ResponseStatusException;
|
|
|
|
/**
|
|
* Cross-tenant 어드민 엔드포인트 진입 가드.
|
|
*
|
|
* 정적 헬퍼 두 개. 컨트롤러는 이 둘을 호출해 boolean 으로 검사 후
|
|
* 명시적으로 {@link org.springframework.http.ResponseEntity} 반환한다.
|
|
* 예외 throw 방식을 안 쓰는 이유 — {@link com.erp.config.GlobalExceptionHandler} 의
|
|
* catch-all 핸들러가 모든 예외를 500 으로 변환하므로, 가드 결과를 정확한 status code 로
|
|
* 내려주려면 컨트롤러가 직접 결정해야 함.
|
|
*
|
|
* SecurityConfig 단계에서 매처로 가두지 않는 이유는 기존 95개 컨트롤러가
|
|
* permitAll 로 동작 중이기 때문 ({@code SecurityConfig} L52~56 코멘트 참조).
|
|
*
|
|
* @see com.erp.crosstenant.CrossTenantAggregator
|
|
* @see com.erp.tenant.DbContextHolder
|
|
*/
|
|
public final class CrossTenantContext {
|
|
|
|
public static final String ROLE_SUPER_ADMIN = "SUPER_ADMIN";
|
|
|
|
private CrossTenantContext() {}
|
|
|
|
/**
|
|
* JWT 가 세팅한 role attribute 가 SUPER_ADMIN 인지.
|
|
* JwtAuthenticationFilter 가 토큰 검증 후 {@code request.setAttribute("role", userType)} 박음.
|
|
* 토큰 없거나 role 미스매치면 false.
|
|
*/
|
|
public static boolean isSuperAdmin(HttpServletRequest request) {
|
|
Object role = request.getAttribute("role");
|
|
return ROLE_SUPER_ADMIN.equals(role);
|
|
}
|
|
|
|
/**
|
|
* 현재 요청이 META DB 컨텍스트인지.
|
|
* SubdomainResolverFilter 가 admin.invyone.com / 메인 도메인일 때 setMeta() 박음.
|
|
* 회사 도메인 (qnc.invyone.com 등) 에서 호출되면 false.
|
|
*/
|
|
public static boolean isMetaContext() {
|
|
return DbContextHolder.isMeta();
|
|
}
|
|
|
|
/**
|
|
* 관리 호스트(solution.invyone.com / admin.invyone.com / localhost / 베이스 도메인) 외엔 거절.
|
|
* cross-tenant 작업은 plane 격리상 관리 호스트에서만 허용. SuperAdminGuard.isTenantHost 와 동일 규칙.
|
|
*/
|
|
public static void requireManagementHost(HttpServletRequest request) {
|
|
String host = request.getHeader("Host");
|
|
if (com.erp.provisioning.SuperAdminGuard.isTenantHost(host)) {
|
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
|
"Cross-tenant operations are only available on the management site");
|
|
}
|
|
}
|
|
}
|