[Developer] #567 use backoffice account for SQL evidence

This commit is contained in:
devmrko
2026-07-01 14:59:22 +09:00
parent 064a5bc31d
commit 1c66b973a7
5 changed files with 21 additions and 42 deletions

View File

@@ -152,4 +152,4 @@ Oracle은 VPD의 내부 rewrite 결과를 별도 최종 SQL 문자열로 `V$SQL`
최근 SQL 매칭은 대상 객체와 최근 2분 실행 시각으로 고르므로, 같은 객체를 동시에 호출하는 운영 환경에서는 다른 요청 cursor가 선택될 가능성이 있다. 현재 UI는 이를 “최근 실행 증적”으로 명시한다. 요청별 완전 상관이 필요해지면 ORDS Handler에 검증 요청 ID를 주입해 고유 SQL comment/module-action으로 cursor를 추적하는 후속 작업으로 확장한다.
실행 증적 연결에는 최소한 `V$SQL``DBMS_XPLAN.DISPLAY_CURSOR`를 조회할 수 있는 catalog 권한이 필요하다. 해당 권한은 ORDS parsing schema 연결에서 먼저 조회하고, 사용할 수 없으면 백오피스 연결로 한 번 더 시도한다. Autonomous의 일반 `ADMIN` 계정은 `SYS.V_$SQL` 권한을 다른 계정에 위임하지 못할 수 있으므로, 이 경우에는 DBA가 [34_agent_ords_execution_evidence_grant.sql](../../sql/adb/34_agent_ords_execution_evidence_grant.sql)을 실행해야 한다. cursor 미발견과 catalog 권한 미보유는 UI에서 서로 다른 안내로 표시하며, 어느 경우도 권한 검증 결과를 실패시키지 않는다.
실행 증적 연결에는 최소한 `V$SQL``DBMS_XPLAN.DISPLAY_CURSOR`를 조회할 수 있는 catalog 권한이 필요하다. 증적은 ORDS parsing schema가 아니라 화면을 표시하는 `BACKOFFICE_DB_USERNAME` 연결에서 조회한다. 실제 SQL 실행 계정과 진단 조회 계정은 달라도 된다. Autonomous의 일반 `ADMIN` 계정은 `SYS.V_$SQL` 권한을 다른 계정에 위임하지 못할 수 있으므로, 이 경우에는 DBA가 [34_agent_ords_execution_evidence_grant.sql](../../sql/adb/34_agent_ords_execution_evidence_grant.sql)을 실행해야 한다. cursor 미발견과 catalog 권한 미보유는 UI에서 서로 다른 안내로 표시하며, 어느 경우도 권한 검증 결과를 실패시키지 않는다.

View File

@@ -4,20 +4,25 @@
--
-- Run as SYS or a DBA account that can grant SYS.V_$ dynamic-performance
-- views. Typical Autonomous ADMIN accounts cannot delegate these views.
-- The grants are read-only and scoped to the ORDS parsing schema. They do
-- not grant table DML or alter DBMS_RLS enforcement.
-- The grants are read-only and scoped to the BACKOFFICE_DB_USERNAME account
-- that displays the evidence (for example, ADMIN). They do not grant table
-- DML or alter DBMS_RLS enforcement.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
PROMPT === Granting ORDS cursor-evidence read access ===
GRANT SELECT ON V_$SQL TO CB_ORDS;
GRANT SELECT ON V_$SQL_PLAN TO CB_ORDS;
GRANT SELECT ON V_$SQL_PLAN_STATISTICS_ALL TO CB_ORDS;
GRANT SELECT ON V_$SESSION TO CB_ORDS;
GRANT EXECUTE ON DBMS_XPLAN TO CB_ORDS;
-- Change ADMIN when BACKOFFICE_DB_USERNAME uses a dedicated observer account.
DEFINE diagnostic_user = ADMIN
PROMPT === ORDS cursor-evidence grants ready ===
PROMPT === Granting backoffice cursor-evidence read access ===
GRANT SELECT ON V_$SQL TO &diagnostic_user;
GRANT SELECT ON V_$SQL_PLAN TO &diagnostic_user;
GRANT SELECT ON V_$SQL_PLAN_STATISTICS_ALL TO &diagnostic_user;
GRANT SELECT ON V_$SESSION TO &diagnostic_user;
GRANT EXECUTE ON DBMS_XPLAN TO &diagnostic_user;
PROMPT === Backoffice cursor-evidence grants ready ===
PROMPT SQL_ID evidence will appear after the next /probe call.
UNDEFINE diagnostic_user
EXIT;

View File

@@ -94,16 +94,6 @@ public class OrdsMetadataService {
.toList();
}
/**
* Uses the optional CB_ORDS metadata connection when configured. SQL cursor
* evidence must be read from the parsing schema's database context, rather
* than from an unrelated backoffice connection.
*/
public JdbcTemplate executionEvidenceJdbcTemplate() {
return ordsMetadataJdbcTemplate;
}
public String objectQueryHandlerSource(long objectId) {
ProtectedObject object = protectedObjectService.assertEnabled(objectId);
rejectGenericVectorHandler(object);

View File

@@ -60,7 +60,6 @@ public class OrdsProbeService {
private final ObjectMapper objectMapper;
private final SettingService settingService;
private final JdbcTemplate jdbcTemplate;
private final OrdsMetadataService ordsMetadataService;
private final Clock clock;
public OrdsProbeService(
@@ -72,7 +71,6 @@ public class OrdsProbeService {
ObjectMapper objectMapper,
SettingService settingService,
JdbcTemplate jdbcTemplate,
OrdsMetadataService ordsMetadataService,
Clock clock
) {
this.tokenService = tokenService;
@@ -83,7 +81,6 @@ public class OrdsProbeService {
this.objectMapper = objectMapper;
this.settingService = settingService;
this.jdbcTemplate = jdbcTemplate;
this.ordsMetadataService = ordsMetadataService;
this.clock = clock;
}
@@ -259,8 +256,8 @@ public class OrdsProbeService {
if (evidence == null) {
if (lookup.sqlCatalogAccessUnavailable()) {
return result.withExecutionEvidence(null,
"SQL_ID 실행 증적을 수집할 권한이 없습니다. 현재 권한 검증 결과는 정상입니다. "
+ "DBA에게 ORDS 실행 계정의 V$SQL DBMS_XPLAN 조회 권한을 요청하세요.");
"백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다. 현재 권한 검증 결과는 정상입니다. "
+ "V$SQL DBMS_XPLAN은 일반 데이터 접근과 별도인 SYS 진단 권한입니다.");
}
return result.withExecutionEvidence(null,
"최근 2분 내 이 보호 대상의 실행 cursor를 찾지 못했습니다. "
@@ -279,24 +276,11 @@ public class OrdsProbeService {
}
private ExecutionEvidenceLookup findRecentExecutionEvidence(ProtectedObject object) {
JdbcTemplate evidenceJdbcTemplate = ordsMetadataService.executionEvidenceJdbcTemplate();
boolean sqlCatalogAccessUnavailable = false;
try {
SqlExecutionEvidence evidence = findRecentExecutionEvidence(evidenceJdbcTemplate, object);
if (evidence != null || evidenceJdbcTemplate == jdbcTemplate) {
return new ExecutionEvidenceLookup(evidence, false);
}
} catch (RuntimeException exception) {
sqlCatalogAccessUnavailable = isSqlCatalogAccessUnavailable(exception);
log.debug("ORDS parsing-schema cursor evidence is unavailable: {}", exception.getMessage());
}
try {
return new ExecutionEvidenceLookup(
findRecentExecutionEvidence(jdbcTemplate, object), sqlCatalogAccessUnavailable);
return new ExecutionEvidenceLookup(findRecentExecutionEvidence(jdbcTemplate, object), false);
} catch (RuntimeException exception) {
log.debug("Backoffice cursor evidence is unavailable: {}", exception.getMessage());
return new ExecutionEvidenceLookup(null,
sqlCatalogAccessUnavailable || isSqlCatalogAccessUnavailable(exception));
return new ExecutionEvidenceLookup(null, isSqlCatalogAccessUnavailable(exception));
}
}

View File

@@ -38,7 +38,7 @@ class ProbeResultTemplateRenderTest {
"{}",
"{}"
).withExecutionEvidence(null,
"SQL_ID 실행 증적을 수집할 권한이 없습니다. 현재 권한 검증 결과는 정상입니다.");
"백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다. 현재 권한 검증 결과는 정상입니다.");
var context = new Context(Locale.KOREAN);
context.setVariable("result", result);
context.setVariable("vectorSearch", false);
@@ -47,6 +47,6 @@ class ProbeResultTemplateRenderTest {
String rendered = engine.process("fragments/probe-result", context);
assertThat(rendered).contains("SQL_ID 실행 증적을 수집할 권한이 없습니다.");
assertThat(rendered).contains("백오피스 DB 연결이 실제 DB cursor SQL을 읽지 못했습니다.");
}
}