diff --git a/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp b/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp index bc6c93b..3413aec 100644 --- a/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp +++ b/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp @@ -183,6 +183,17 @@ var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용) + // 값이 있을 때만 행을 노출한다. (해지 신청처럼 해당 항목이 없는 유형에서 빈 행이 남지 않도록) + function setOptionalRow(rowId, spanId, value) { + if (value) { + $("#" + spanId).text(value); + $("#" + rowId).show(); + } else { + $("#" + spanId).text(""); + $("#" + rowId).hide(); + } + } + function detail(key) { $.ajax({ type: "POST", @@ -264,6 +275,11 @@ $("#clientName").text(data.clientName); + // 신청서에 담긴 Client 등록 정보(승인 시 GW 클라이언트로 반영되는 값). 값이 있을 때만 노출 + setOptionalRow("appDescriptionRow", "appDescription", data.appDescription); + setOptionalRow("ipWhitelistRow", "ipWhitelist", data.ipWhitelist); + setOptionalRow("callbackUrlRow", "callbackUrl", data.callbackUrl); + // 신청 사유. 해지 신청은 사유 입력이 필수이므로 "해지 사유"로 표기하고, 그 외 유형은 "신청 사유" if (data.requestReason) { $("#requestReasonLabel").text(data.requestType === 'DELETE' ? "해지 사유" : "신청 사유"); @@ -375,6 +391,18 @@ 이름 + + 앱 설명 + + + + IP 화이트리스트 + + + + Callback URL + + 신청 사유 diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManService.java index e74f828..46173cb 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManService.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManService.java @@ -246,6 +246,10 @@ public class PortalApprovalManService extends BaseService { appApprovalUI.setClientName(appRequest.getClientName()); // 신청 사유는 상세 내용과 별개 항목으로도 내려준다(해지 신청 화면에서 "해지 사유"로 표기) appApprovalUI.setRequestReason(appRequest.getReason()); + // 승인 시 GW 클라이언트로 그대로 반영되는 항목이므로 승인자가 검토할 수 있게 함께 내려준다. + appApprovalUI.setIpWhitelist(appRequest.getIpWhitelist()); + appApprovalUI.setCallbackUrl(appRequest.getCallbackUrl()); + appApprovalUI.setAppDescription(appRequest.getAppDescription()); appApprovalUI.getApprovers().forEach(approver -> { if (approver.getUser() == null) { diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/GwClientSyncService.java b/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/GwClientSyncService.java new file mode 100644 index 0000000..f18cc84 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/GwClientSyncService.java @@ -0,0 +1,84 @@ +package com.eactive.eai.rms.onl.apim.approval.app; + +import com.eactive.apim.portal.apprequest.entity.AppRequest; +import com.eactive.apim.portal.apprequest.entity.AppRequestType; +import com.eactive.apim.portal.apprequest.entity.GwAction; +import com.eactive.apim.portal.approval.service.ApprovalDeployException; +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceType; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI; +import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper; +import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +/** + * APP 승인 시 GW 인증 클라이언트(APIGW 스키마 TSEAIAU01) 반영을 승인 트랜잭션과 분리된 새 트랜잭션에서 + * 수행하고 커밋까지 끝낸다. + * + *

분리가 필요한 이유: GW 는 RELOAD_CLIENT 명령을 받으면 자기 커넥션으로 DB 를 다시 읽어 메모리에 적재한다 + * ({@code OAuth2Manager#reloadClient} 는 캐시에서 제거 후 {@code getClientById} 결과가 있을 때만 다시 넣는다). + * 승인 트랜잭션 안에서 INSERT 한 직후 broadcast 하면 그 행이 아직 커밋되지 않아 GW 가 찾지 못하고, + * 신규(NEW) 클라이언트는 캐시에 올라가지 못한 채 리로드가 무의미해진다. 변경(MODIFY)도 커밋 전 옛 값이 + * 다시 적재된다. 따라서 broadcast 는 반드시 이 메서드가 반환(=커밋)된 뒤에 수행해야 한다. + * + *

트랜잭션 매니저는 APIGW 스키마를 라우팅하는 멀티테넌트 EMF 쪽({@code transactionManager})을 사용한다. + * Hibernate 는 Session 을 열 때 테넌트를 한 번만 해석하므로, 새 트랜잭션을 여는 이 시점에 + * {@link DataSourceContextHolder} 가 APIGW 로 설정돼 있어야 한다. + */ +@Service +@RequiredArgsConstructor +public class GwClientSyncService { + + private static final Logger logger = LoggerFactory.getLogger(GwClientSyncService.class); + + private final ClientManService clientManService; + private final CredentialUIMapper credentialUIMapper; + + @Transactional(transactionManager = "transactionManager", + propagation = Propagation.REQUIRES_NEW, + rollbackFor = ApprovalDeployException.class) + public void apply(AppRequest appRequest, CredentialUI credentialUI) throws ApprovalDeployException { + // 새 Session 이 열리는 시점의 테넌트가 APIGW 여야 TSEAIAU01 에 접근할 수 있다. + DataSourceType type = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW); + DataSourceContextHolder.setDataSourceType(type); + + AppRequestType action = appRequest.getType(); + if (AppRequestType.NEW.equals(action)) { + clientManService.insert(credentialUIMapper.mapClient(credentialUI)); + } else if (AppRequestType.MODIFY.equals(action)) { + clientManService.update(credentialUIMapper.mapClient(credentialUI)); + } else if (AppRequestType.DELETE.equals(action)) { + applyGwActionForDelete(appRequest.getGwAction(), credentialUI.getClientid()); + } + } + + /** + * API 이용해지(DELETE) 승인 시 GW 클라이언트(TSEAIAU01) 처리. + * 승인 시점에 관리자가 선택해 PTL_APP_REQUEST.GW_ACTION 에 영속화된 값을 따른다. + * - DELETE: 행 완전 삭제 / - BLOCK 또는 null(미지정): 차단(appstatus='0') + * GW 에 행이 이미 없으면(선삭제·재처리) 목적이 달성된 상태이므로 통과시키고, + * 그 외 오류는 ApprovalDeployException 으로 던져 FAILED(재처리 가능) 상태를 만든다. + */ + private void applyGwActionForDelete(GwAction gwAction, String clientId) throws ApprovalDeployException { + try { + if (GwAction.DELETE.equals(gwAction)) { + clientManService.delete(clientId); + logger.info("GW 클라이언트 완전 삭제 - clientId: {}", clientId); + } else { + clientManService.blockClient(clientId); + logger.info("GW 클라이언트 차단(appstatus=0) - clientId: {}", clientId); + } + } catch (org.springframework.dao.EmptyResultDataAccessException | javax.persistence.EntityNotFoundException e) { + logger.warn("GW 클라이언트가 이미 존재하지 않아 처리 생략 (재처리 통과) - clientId: {}", clientId); + } catch (RuntimeException e) { + logger.error("GW 클라이언트 해지 처리 실패 - clientId: {}, gwAction: {}", clientId, gwAction, e); + throw new ApprovalDeployException("GW 클라이언트 해지 처리에 실패했습니다: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalListener.java b/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalListener.java index 247eeaf..0440c8d 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalListener.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalListener.java @@ -2,7 +2,6 @@ package com.eactive.eai.rms.onl.apim.approval.app; import com.eactive.apim.portal.apprequest.entity.AppRequest; import com.eactive.apim.portal.apprequest.entity.AppRequestType; -import com.eactive.apim.portal.apprequest.entity.GwAction; import com.eactive.apim.portal.apprequest.repository.AppRequestRepository; import com.eactive.apim.portal.approval.entity.Approval; import com.eactive.apim.portal.approval.service.ApprovalDeployException; @@ -10,6 +9,7 @@ import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener; import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.apim.portal.template.service.MessageRecipient; import com.eactive.apim.portal.template.service.MessageSendService; +import com.eactive.eai.agent.command.CommandResult; import com.eactive.eai.agent.command.CommonCommand; import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; import com.eactive.eai.rms.common.datasource.DataSourceType; @@ -18,9 +18,7 @@ import com.eactive.eai.rms.data.entity.onl.apim.apigroup.ApiGroupService; import com.eactive.eai.rms.onl.apim.approval.RandomStringGenerator; import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService; import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI; -import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper; import com.eactive.eai.rms.onl.common.service.AgentUtilService; -import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService; import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI; import java.util.Arrays; import java.util.Collections; @@ -55,9 +53,8 @@ public class PortalAppApprovalListener implements ApprovalListener { private final ApiGroupService apiGroupService; private final AppRequestRepository appRequestRepository; private final CredentialManService credentialManService; - private final CredentialUIMapper credentialUIMapper; - private final ClientManService clientManService; private final MessageSendService messageSendService; + private final GwClientSyncService gwClientSyncService; @Override @@ -127,47 +124,23 @@ public class PortalAppApprovalListener implements ApprovalListener { private void syncTargetServer(AppRequest appRequest, CredentialUI credentialUI) throws ApprovalDeployException { - AppRequestType action = appRequest.getType(); - - DataSourceType type = DataSourceTypeManager.getDataSourceType("APIGW"); + // TSEAIAU01/TSEAISY02 는 APIGW 스키마에 있으므로 라우팅을 먼저 설정한다. + // (GW 반영은 새 트랜잭션에서, 이어지는 broadcast 의 서버목록 조회도 이 테넌트로 수행된다) + DataSourceType type = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW); DataSourceContextHolder.setDataSourceType(type); - if (action.equals(AppRequestType.NEW)) { - clientManService.insert(credentialUIMapper.mapClient(credentialUI)); - } else if (action.equals(AppRequestType.MODIFY)) { - clientManService.update(credentialUIMapper.mapClient(credentialUI)); - } else if (action.equals(AppRequestType.DELETE)) { - applyGwActionForDelete(appRequest.getGwAction(), credentialUI.getClientid()); - } + // GW 반영은 별도 트랜잭션에서 커밋까지 끝낸다. GW 는 리로드 명령을 받고 DB 를 다시 읽으므로 + // 커밋 전에 broadcast 하면 신규 클라이언트를 찾지 못해 리로드가 무의미해진다. + gwClientSyncService.apply(appRequest, credentialUI); - CommonCommand.builder() + // broadcast 는 서버별 전송 실패를 예외로 던지지 않고 결과 맵에 담아 정상 반환한다. + // 결과를 버리면 리로드 실패가 아무 흔적 없이 묻히므로 서버별 응답을 반드시 남긴다. + CommandResult reloadResult = CommonCommand.builder() .name(CommonCommand.RELOAD_CLIENT_COMMAND) .args(credentialUI.getClientid()) .build().broadcast(agentUtilService); - } - - /** - * API 이용해지 승인 시 GW 클라이언트(TSEAIAU01) 처리. - * 승인 시점에 관리자가 선택해 PTL_APP_REQUEST.GW_ACTION 에 영속화된 값을 따른다. - * - DELETE: 행 완전 삭제 / - BLOCK 또는 null(미지정): 차단(appstatus='0') - * GW 에 행이 이미 없으면(선삭제·재처리) 목적이 달성된 상태이므로 통과시키고, - * 그 외 오류는 ApprovalDeployException 으로 던져 FAILED(재처리 가능) 상태를 만든다. - */ - private void applyGwActionForDelete(GwAction gwAction, String clientId) throws ApprovalDeployException { - try { - if (GwAction.DELETE.equals(gwAction)) { - clientManService.delete(clientId); - logger.info("GW 클라이언트 완전 삭제 - clientId: {}", clientId); - } else { - clientManService.blockClient(clientId); - logger.info("GW 클라이언트 차단(appstatus=0) - clientId: {}", clientId); - } - } catch (org.springframework.dao.EmptyResultDataAccessException | javax.persistence.EntityNotFoundException e) { - logger.warn("GW 클라이언트가 이미 존재하지 않아 처리 생략 (재처리 통과) - clientId: {}", clientId); - } catch (RuntimeException e) { - logger.error("GW 클라이언트 해지 처리 실패 - clientId: {}, gwAction: {}", clientId, gwAction, e); - throw new ApprovalDeployException("GW 클라이언트 해지 처리에 실패했습니다: " + e.getMessage()); - } + logger.info("GW 클라이언트 리로드 broadcast - clientId: {}, 서버별 응답: {}", + credentialUI.getClientid(), reloadResult.getMessage(false)); } private CredentialUI modifyClientUI(AppRequest appRequest) { diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalUI.java index b291ac2..d7c0a6f 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalUI.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/approval/app/PortalAppApprovalUI.java @@ -33,4 +33,9 @@ public class PortalAppApprovalUI extends PortalApprovalUI { //신청자가 입력한 사유(PTL_APP_REQUEST.REASON). 해지 신청은 사유가 필수라 별도 항목으로 노출한다. private String requestReason; + + //신청서(PTL_APP_REQUEST)에 담긴 Client 등록 정보. 승인 시 GW(TSEAIAU01)로 그대로 반영되므로 승인 화면에 노출한다. + private String ipWhitelist; + private String callbackUrl; + private String appDescription; }