diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/ApiPermissionFilter.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/ApiPermissionFilter.java
index 3f3066d..9ae30e9 100644
--- a/src/main/java/com/eactive/apim/portal/apps/auth/service/ApiPermissionFilter.java
+++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/ApiPermissionFilter.java
@@ -64,33 +64,36 @@ public class ApiPermissionFilter {
.collect(Collectors.toList());
}
- private static boolean checkServicePermissions(ApiServiceDTO spec, boolean isAuthenticated,
+ /**
+ * org 축과 role 축은 각자 독립적으로 선택적 제한이다(비어있으면 그 축은 통과) — 둘을 AND로 합친다.
+ * (과거 OR 합성은 한쪽 축이 비어있으면 그 축이 무조건 true가 되어 다른 쪽 제한을 무력화시키는
+ * 결함이 있었다 — 예: displayOrg 만 설정하고 displayRoleCode 를 비워두면 roleMatch 가 로그인
+ * 여부만으로 true 가 되어 조직 제한이 사실상 적용되지 않았다.)
+ */
+ static boolean checkServicePermissions(ApiServiceDTO spec, boolean isAuthenticated,
String org, String roleCode) {
if (!isAuthenticated) {
return (spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()) && (spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty());
}
- boolean orgMatch = org != null && (spec.getDisplayOrg() == null ||
- spec.getDisplayOrg().isEmpty() ||
- spec.getDisplayOrg().contains(org));
- boolean roleMatch = roleCode != null && (spec.getDisplayRoleCode() == null ||
- spec.getDisplayRoleCode().isEmpty() ||
- spec.getDisplayRoleCode().contains(roleCode));
- return orgMatch || roleMatch;
+ boolean orgOk = spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()
+ || (org != null && spec.getDisplayOrg().contains(org));
+ boolean roleOk = spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty()
+ || (roleCode != null && spec.getDisplayRoleCode().contains(roleCode));
+ return orgOk && roleOk;
}
- private static boolean checkApiPermissions(ApiSpecInfoDto spec, boolean isAuthenticated,
+ /** {@link #checkServicePermissions} 와 동일 규칙(중복 유지 — 대상 DTO 타입만 다름). */
+ static boolean checkApiPermissions(ApiSpecInfoDto spec, boolean isAuthenticated,
String org, String roleCode) {
if (!isAuthenticated) {
return (spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()) && (spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty());
}
- boolean orgMatch = org != null && (spec.getDisplayOrg() == null ||
- spec.getDisplayOrg().isEmpty() ||
- spec.getDisplayOrg().contains(org));
- boolean roleMatch = roleCode != null && (spec.getDisplayRoleCode() == null ||
- spec.getDisplayRoleCode().isEmpty() ||
- spec.getDisplayRoleCode().contains(roleCode));
- return orgMatch || roleMatch;
+ boolean orgOk = spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()
+ || (org != null && spec.getDisplayOrg().contains(org));
+ boolean roleOk = spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty()
+ || (roleCode != null && spec.getDisplayRoleCode().contains(roleCode));
+ return orgOk && roleOk;
}
}
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
index bc47090..79189a5 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
@@ -27,13 +27,23 @@ import java.util.Optional;
*
*
공지 조건을 EXISTS 로 쓰는 이유: 예전처럼 {@code FROM ... , PortalNotice n} 으로 조인하면
* NOTICE_ID 가 없는 지연 이슈가 행 자체에서 사라진다.
+ *
+ * API 그룹으로 특정 법인에게만 공개된 API 의 이슈는, 그 API 를 조회할 수 없는 사용자에게는
+ * 이슈 자체(제목·요약·타임라인 포함)를 노출하지 않는다. 영향 API 가 하나도 없는 이슈(전사 공지성)는
+ * 그대로 노출한다. {@code visibleApiIds} 는 {@code ApiStatusCatalogService#getVisibleApiIdsForQuery()}
+ * 로 구한, 현재 사용자에게 공개된 API ID 집합이다.
*/
public interface ApiStatusIncidentQueryRepository extends Repository {
String VISIBLE = " i.draftYn = 'N'"
+ " AND (i.noticeId IS NULL"
+ " OR EXISTS (SELECT 1 FROM PortalNotice n"
- + " WHERE n.id = i.noticeId AND n.useYn = 'Y')) ";
+ + " WHERE n.id = i.noticeId AND n.useYn = 'Y'))"
+ + " AND (NOT EXISTS (SELECT 1 FROM DjbApistatusIncidentApi ia"
+ + " WHERE ia.incidentId = i.incidentId)"
+ + " OR EXISTS (SELECT 1 FROM DjbApistatusIncidentApi ia2"
+ + " WHERE ia2.incidentId = i.incidentId"
+ + " AND ia2.apiId IN :visibleApiIds)) ";
/** 종결 판정이 STATE 로 이뤄지는 종류 (장애·지연). JPQL 리터럴로 써야 해서 FQCN 을 쓴다 */
String KIND_INCIDENT = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.INCIDENT";
@@ -55,7 +65,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository findVisibleOpenIncidents(@Param("kinds") Collection kinds,
- @Param("closedStates") Collection closedStates);
+ @Param("closedStates") Collection closedStates,
+ @Param("visibleApiIds") Collection visibleApiIds);
/**
* 예정/진행 중 점검 (P5). 종료 시각이 없거나 아직 지나지 않은 점검.
@@ -66,7 +77,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository= :now)"
+ " ORDER BY i.startedAt ASC")
List findVisibleOngoingMaintenance(@Param("kind") IncidentKind kind,
- @Param("now") LocalDateTime now);
+ @Param("now") LocalDateTime now,
+ @Param("visibleApiIds") Collection visibleApiIds);
/**
* 종결된 이슈 (P6). 장애·지연은 종결 상태, 점검은 종료 시각 경과.
@@ -80,6 +92,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository findVisibleClosedIssues(@Param("closedStates") Collection closedStates,
@Param("now") LocalDateTime now,
+ @Param("visibleApiIds") Collection visibleApiIds,
Pageable pageable);
/**
@@ -92,7 +105,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository= :from)"
+ " ORDER BY i.startedAt DESC")
List findVisibleOverlapping(@Param("from") LocalDateTime from,
- @Param("to") LocalDateTime to);
+ @Param("to") LocalDateTime to,
+ @Param("visibleApiIds") Collection visibleApiIds);
/**
* 기간과 겹치고 특정 API 에 영향을 준 이슈 (P9/P10 의 apiId 필터).
@@ -106,7 +120,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository findVisibleOverlappingByApi(@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to,
- @Param("apiId") String apiId);
+ @Param("apiId") String apiId,
+ @Param("visibleApiIds") Collection visibleApiIds);
/**
* 공개 상세 (P7)
@@ -114,5 +129,6 @@ public interface ApiStatusIncidentQueryRepository extends Repository findVisibleById(@Param("incidentId") Long incidentId);
+ Optional findVisibleById(@Param("incidentId") Long incidentId,
+ @Param("visibleApiIds") Collection visibleApiIds);
}
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
index 6293cf0..19ba3ba 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
@@ -152,7 +152,8 @@ public class ApiCurrentStatusService {
/** 진행 중(미종결) 장애·지연 중 API 별로 가장 심각한 한 건 */
private Map mapOpenIncidents(Set apiIds) {
List openIncidents = incidentQueryRepository
- .findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
+ .findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES,
+ catalogService.getVisibleApiIdsForQuery());
if (openIncidents.isEmpty()) {
return Collections.emptyMap();
}
@@ -178,7 +179,8 @@ public class ApiCurrentStatusService {
/** 이미 시작된 점검 중 API 별로 가장 먼저 시작된 한 건 (예정 점검은 현재 상태가 아니므로 제외) */
private Map mapStartedMaintenance(Set apiIds, LocalDateTime now) {
List maintenances = incidentQueryRepository
- .findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, now).stream()
+ .findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, now,
+ catalogService.getVisibleApiIdsForQuery()).stream()
.filter(incident -> incident.getStartedAt() != null && !incident.getStartedAt().isAfter(now))
.collect(Collectors.toList());
if (maintenances.isEmpty()) {
@@ -208,7 +210,8 @@ public class ApiCurrentStatusService {
private Map collectLastIncidentAt(Set apiIds, LocalDateTime now, int windowDays) {
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
List incidents = incidentQueryRepository
- .findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
+ .findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay(),
+ catalogService.getVisibleApiIdsForQuery()).stream()
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
.collect(Collectors.toList());
if (incidents.isEmpty()) {
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java
index cb9bc8b..d3f109f 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java
@@ -16,6 +16,7 @@ import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.Instant;
import java.time.LocalDateTime;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
@@ -99,6 +100,19 @@ public class ApiStatusCatalogService {
return names;
}
+ /** IN 절 바인딩용 API ID 없을 리 없는 더미값. 실제 API ID 는 절대 이 값이 될 수 없다 */
+ private static final List NO_VISIBLE_API = Collections.singletonList("__NO_VISIBLE_API__");
+
+ /**
+ * {@link ApiStatusIncidentQueryRepository} 의 {@code :visibleApiIds} 바인딩용.
+ * Oracle 은 빈 컬렉션으로 {@code IN ()} 을 만들면 구문 오류가 나므로, 조회 가능한 API 가
+ * 하나도 없을 때는 절대 매치되지 않는 더미값으로 대체한다.
+ */
+ public Collection getVisibleApiIdsForQuery() {
+ Map names = getVisibleApiNames();
+ return names.isEmpty() ? NO_VISIBLE_API : names.keySet();
+ }
+
/**
* API 상태 모니터링 Job(eapim-admin Quartz)의 마지막 실행 시각.
*
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
index 3c5eb8e..e168075 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
@@ -130,9 +130,10 @@ public class ApiStatusIssueHistoryService {
private List findOverlapping(LocalDateTime from, LocalDateTime to,
String apiId, String kind) {
+ java.util.Collection visibleApiIds = catalogService.getVisibleApiIdsForQuery();
List incidents = StringUtils.isBlank(apiId)
- ? incidentQueryRepository.findVisibleOverlapping(from, to)
- : incidentQueryRepository.findVisibleOverlappingByApi(from, to, apiId);
+ ? incidentQueryRepository.findVisibleOverlapping(from, to, visibleApiIds)
+ : incidentQueryRepository.findVisibleOverlappingByApi(from, to, apiId, visibleApiIds);
java.util.function.Predicate filter = kindFilter(kind);
if (filter == null) {
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
index 7ac0314..46a6183 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
@@ -25,19 +25,22 @@ public class ApiStatusQueryService {
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
private final ApiStatusAssembler assembler;
+ private final ApiStatusCatalogService catalogService;
/** P3 - 진행 중 장애·지연 */
public List getActiveIncidents() {
LocalDateTime now = ApiStatusSupport.now();
List incidents = incidentQueryRepository
- .findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
+ .findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES,
+ catalogService.getVisibleApiIdsForQuery());
return assembler.toActiveIncidents(incidents, now);
}
/** P5 - 예정/진행 중 점검 */
public List getOngoingMaintenance() {
List incidents = incidentQueryRepository
- .findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, ApiStatusSupport.now());
+ .findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, ApiStatusSupport.now(),
+ catalogService.getVisibleApiIdsForQuery());
return assembler.toMaintenanceCards(incidents);
}
@@ -46,14 +49,14 @@ public class ApiStatusQueryService {
int limit = size <= 0 ? 5 : Math.min(size, 50);
List incidents = incidentQueryRepository
.findVisibleClosedIssues(ApiStatusSupport.CLOSED_STATES, ApiStatusSupport.now(),
- PageRequest.of(0, limit))
+ catalogService.getVisibleApiIdsForQuery(), PageRequest.of(0, limit))
.getContent();
return assembler.toPastIssueCards(incidents);
}
/** P7 - 이슈 공개 상세 */
public Optional getIssueDetail(Long incidentId) {
- return incidentQueryRepository.findVisibleById(incidentId)
+ return incidentQueryRepository.findVisibleById(incidentId, catalogService.getVisibleApiIdsForQuery())
.map(incident -> assembler.toPastIssueCards(java.util.Collections.singletonList(incident)).get(0));
}
}
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
index c51cf68..85c5e35 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
@@ -34,6 +34,7 @@ public class ApiStatusUptimeService {
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
private final DjbApistatusIncidentApiRepository incidentApiRepository;
+ private final ApiStatusCatalogService catalogService;
/** P2 - 90일 가동률 */
public List getDailyStats(int days) {
@@ -43,7 +44,8 @@ public class ApiStatusUptimeService {
LocalDate from = today.minusDays(windowDays - 1L);
List incidents = incidentQueryRepository
- .findVisibleOverlapping(from.atStartOfDay(), today.plusDays(1).atStartOfDay());
+ .findVisibleOverlapping(from.atStartOfDay(), today.plusDays(1).atStartOfDay(),
+ catalogService.getVisibleApiIdsForQuery());
List result = new ArrayList<>();
for (int offset = 0; offset < windowDays; offset++) {
@@ -66,7 +68,8 @@ public class ApiStatusUptimeService {
// 장애와 지연 모두 서비스 저하이므로 가동률에서 차감한다. 점검은 계획된 작업이라 제외.
List incidents = incidentQueryRepository
- .findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
+ .findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay(),
+ catalogService.getVisibleApiIdsForQuery()).stream()
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
.collect(Collectors.toList());
diff --git a/src/main/java/com/eactive/apim/portal/djb/notitest/controller/NotiTestController.java b/src/main/java/com/eactive/apim/portal/djb/notitest/controller/NotiTestController.java
index 7db5f21..b9a8378 100644
--- a/src/main/java/com/eactive/apim/portal/djb/notitest/controller/NotiTestController.java
+++ b/src/main/java/com/eactive/apim/portal/djb/notitest/controller/NotiTestController.java
@@ -44,7 +44,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Controller
-@RequestMapping("/djb/notitest")
+@RequestMapping("//notitest")
@RequiredArgsConstructor
public class NotiTestController {
diff --git a/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java b/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java
index c392cff..322a730 100644
--- a/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java
+++ b/src/main/java/com/eactive/apim/portal/djb/testcleanup/controller/TestCleanupInternalController.java
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.djb.testcleanup.controller;
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
import com.eactive.apim.portal.common.util.IpAddressMatcher;
+import com.eactive.apim.portal.djb.testcleanup.service.GroupRestrictedIncidentFixture;
import com.eactive.apim.portal.djb.testcleanup.service.OrphanCleanupService;
import com.eactive.apim.portal.djb.testcleanup.service.TestCleanupResult;
import com.eactive.apim.portal.djb.testcleanup.service.TestCleanupService;
@@ -310,6 +311,105 @@ public class TestCleanupInternalController {
return ResponseEntity.ok(body);
}
+ /**
+ * API Status 그룹공개(법인전용) 필터 검증용 "다른 법인" 계정을 준비한다. 지정 사업자번호의
+ * 법인이 없으면 새로 만들고(승인 절차 없이 즉시 로그인 가능), 있으면 매니저 계정을 재소속·
+ * 비밀번호 재설정으로 되돌린다(heal). 정리는 기존 {@code /internal/test-cleanup/org} 를
+ * 그대로 재사용한다(같은 compRegNo).
+ */
+ @PostMapping("/secondary-org")
+ public ResponseEntity