Consolidate data access control backoffice updates
This commit is contained in:
@@ -36,7 +36,16 @@ backoffice:
|
||||
security:
|
||||
admin-user: ${BACKOFFICE_ADMIN_USER:admin}
|
||||
admin-password: ${BACKOFFICE_ADMIN_PASSWORD:admin}
|
||||
# A stable encoded value keeps signed remember-me cookies valid across restarts.
|
||||
admin-password-hash: ${BACKOFFICE_ADMIN_PASSWORD_HASH:}
|
||||
guest-enabled: ${BACKOFFICE_GUEST_ENABLED:false}
|
||||
guest-user: ${BACKOFFICE_GUEST_USER:guest}
|
||||
guest-password: ${BACKOFFICE_GUEST_PASSWORD:}
|
||||
guest-password-hash: ${BACKOFFICE_GUEST_PASSWORD_HASH:}
|
||||
require-https: ${BACKOFFICE_REQUIRE_HTTPS:false}
|
||||
remember-me-enabled: ${BACKOFFICE_REMEMBER_ME_ENABLED:false}
|
||||
remember-me-key: ${BACKOFFICE_REMEMBER_ME_KEY:}
|
||||
remember-me-days: ${BACKOFFICE_REMEMBER_ME_DAYS:14}
|
||||
token:
|
||||
max-days: ${BACKOFFICE_TOKEN_MAX_DAYS:365}
|
||||
ords:
|
||||
@@ -49,10 +58,13 @@ backoffice:
|
||||
password: ${BACKOFFICE_ORDS_DB_PASSWORD:}
|
||||
ai:
|
||||
enabled: ${BACKOFFICE_AI_ENABLED:false}
|
||||
base-url: ${BACKOFFICE_AI_BASE_URL:}
|
||||
model: ${BACKOFFICE_AI_MODEL:}
|
||||
provider: ${BACKOFFICE_AI_PROVIDER:openai}
|
||||
base-url: ${BACKOFFICE_AI_BASE_URL:${POC3_LLM_GPT55_OCI_ENDPOINT:}}
|
||||
model: ${BACKOFFICE_AI_MODEL:${POC3_LLM_GPT55_OCI_MODEL_ID:}}
|
||||
embedding-model: ${BACKOFFICE_AI_EMBEDDING_MODEL:}
|
||||
api-key: ${BACKOFFICE_AI_API_KEY:}
|
||||
timeout: ${BACKOFFICE_AI_TIMEOUT_SECONDS:30}s
|
||||
mcp:
|
||||
access-token: ${BACKOFFICE_MCP_ACCESS_TOKEN:}
|
||||
oci-config-file: ${BACKOFFICE_AI_OCI_CONFIG_FILE:${OCI_CONFIG_FILE:}}
|
||||
oci-profile: ${BACKOFFICE_AI_OCI_PROFILE:${OCI_PROFILE:DEFAULT}}
|
||||
oci-region: ${BACKOFFICE_AI_OCI_REGION:${POC3_LLM_GPT55_OCI_REGION:}}
|
||||
oci-compartment-id: ${BACKOFFICE_AI_OCI_COMPARTMENT_ID:${OCI_GENAI_COMPARTMENT_ID:}}
|
||||
|
||||
264
src/main/resources/mapper/MaskingRuleMapper.xml
Normal file
264
src/main/resources/mapper/MaskingRuleMapper.xml
Normal file
@@ -0,0 +1,264 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper">
|
||||
<select id="findAllRules" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule">
|
||||
SELECT rule_id, rule_code, rule_name, template_code, description, enabled_yn
|
||||
FROM cb_masking_rule
|
||||
ORDER BY enabled_yn DESC, rule_name, rule_id
|
||||
</select>
|
||||
|
||||
<select id="findEnabledRules" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule">
|
||||
SELECT rule_id, rule_code, rule_name, template_code, description, enabled_yn
|
||||
FROM cb_masking_rule
|
||||
WHERE enabled_yn = 'Y'
|
||||
ORDER BY rule_name, rule_id
|
||||
</select>
|
||||
|
||||
<select id="findRuleById" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule">
|
||||
SELECT rule_id, rule_code, rule_name, template_code, description, enabled_yn
|
||||
FROM cb_masking_rule
|
||||
WHERE rule_id = #{ruleId}
|
||||
</select>
|
||||
|
||||
<select id="findRuleByCode" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule">
|
||||
SELECT rule_id, rule_code, rule_name, template_code, description, enabled_yn
|
||||
FROM cb_masking_rule
|
||||
WHERE rule_code = #{ruleCode}
|
||||
</select>
|
||||
|
||||
<select id="nextRuleId" resultType="long">
|
||||
SELECT NVL(MAX(rule_id), 0) + 1 FROM cb_masking_rule
|
||||
</select>
|
||||
|
||||
<insert id="insertRule">
|
||||
INSERT INTO cb_masking_rule (rule_id, rule_code, rule_name, template_code, description, enabled_yn)
|
||||
VALUES (#{ruleId}, #{command.ruleCode}, #{command.ruleName}, #{command.templateCode},
|
||||
#{command.description}, 'Y')
|
||||
</insert>
|
||||
|
||||
<update id="updateRuleActive">
|
||||
UPDATE cb_masking_rule
|
||||
SET enabled_yn = #{enabledYn}
|
||||
WHERE rule_id = #{ruleId}
|
||||
</update>
|
||||
|
||||
<select id="findColumnRules" resultType="com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule">
|
||||
SELECT c.column_id,
|
||||
c.object_id,
|
||||
o.owner,
|
||||
o.object_name,
|
||||
c.column_name,
|
||||
r.rule_id,
|
||||
r.rule_code,
|
||||
r.rule_name,
|
||||
r.template_code,
|
||||
r.enabled_yn AS rule_enabled_yn
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_protected_column c ON c.column_id = link.column_id
|
||||
JOIN cb_protected_object o ON o.object_id = c.object_id
|
||||
JOIN cb_masking_rule r ON r.rule_id = link.rule_id
|
||||
ORDER BY o.owner, o.object_name, c.column_name
|
||||
</select>
|
||||
|
||||
<!--
|
||||
Runtime truth comes from Oracle's REDACTION_POLICIES / REDACTION_COLUMNS,
|
||||
while CB_* tables represent the backoffice's desired configuration.
|
||||
Keep this comparison limited to the policies managed by this screen.
|
||||
-->
|
||||
<select id="findPolicyStatuses" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus">
|
||||
WITH managed_policy AS (
|
||||
SELECT 'KB_CUSTOMERS' AS object_name, 'KB_CUSTOMER_PII_REDACT' AS policy_name FROM dual
|
||||
UNION ALL SELECT 'KB_CLAIMS', 'KB_CLAIM_AMOUNT_REDACT' FROM dual
|
||||
UNION ALL SELECT 'KB_CONTRACTS', 'KB_CONTRACT_PREMIUM_REDACT' FROM dual
|
||||
UNION ALL SELECT 'KB_EXTERNAL_HOLDINGS', 'KB_EXT_HOLDING_REDACT' FROM dual
|
||||
),
|
||||
configured AS (
|
||||
SELECT protected_object.object_name,
|
||||
COUNT(*) AS configured_column_count
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
WHERE protected_object.owner = 'POC_2'
|
||||
AND rule.enabled_yn = 'Y'
|
||||
GROUP BY protected_object.object_name
|
||||
),
|
||||
database_policy AS (
|
||||
SELECT policy.object_name,
|
||||
policy.policy_name,
|
||||
policy.enable,
|
||||
COUNT(policy_column.column_name) AS applied_column_count
|
||||
FROM redaction_policies policy
|
||||
JOIN managed_policy managed
|
||||
ON managed.object_name = policy.object_name
|
||||
AND managed.policy_name = policy.policy_name
|
||||
LEFT JOIN redaction_columns policy_column
|
||||
ON policy_column.object_owner = policy.object_owner
|
||||
AND policy_column.object_name = policy.object_name
|
||||
WHERE policy.object_owner = 'POC_2'
|
||||
GROUP BY policy.object_name, policy.policy_name, policy.enable
|
||||
),
|
||||
missing_columns AS (
|
||||
SELECT protected_object.object_name,
|
||||
COUNT(*) AS missing_column_count
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
LEFT JOIN redaction_columns policy_column
|
||||
ON policy_column.object_owner = protected_object.owner
|
||||
AND policy_column.object_name = protected_object.object_name
|
||||
AND policy_column.column_name = protected_column.column_name
|
||||
WHERE protected_object.owner = 'POC_2'
|
||||
AND rule.enabled_yn = 'Y'
|
||||
AND policy_column.column_name IS NULL
|
||||
GROUP BY protected_object.object_name
|
||||
),
|
||||
extra_columns AS (
|
||||
SELECT policy_column.object_name,
|
||||
COUNT(*) AS extra_column_count
|
||||
FROM redaction_columns policy_column
|
||||
JOIN managed_policy managed ON managed.object_name = policy_column.object_name
|
||||
WHERE policy_column.object_owner = 'POC_2'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
|
||||
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||
WHERE protected_object.owner = 'POC_2'
|
||||
AND protected_object.object_name = policy_column.object_name
|
||||
AND protected_column.column_name = policy_column.column_name
|
||||
AND rule.enabled_yn = 'Y'
|
||||
)
|
||||
GROUP BY policy_column.object_name
|
||||
),
|
||||
legacy_vpd_column_policy AS (
|
||||
SELECT policy.object_name,
|
||||
COUNT(*) AS legacy_vpd_column_policy_count
|
||||
FROM all_policies policy
|
||||
WHERE policy.object_owner = 'POC_2'
|
||||
AND policy.enable = 'YES'
|
||||
AND policy.policy_name IN (
|
||||
'KB_CUST_NM_CLS_POLICY', 'KB_RRN_CLS_POLICY',
|
||||
'KB_PREMIUM_CLS_POLICY',
|
||||
'KB_CLAIM_AMT_CLS_POLICY', 'KB_PAID_AMT_CLS_POLICY',
|
||||
'KB_EXT_INSURER_CLS_POLICY', 'KB_EXT_PRODUCT_GRP_CLS_POLICY',
|
||||
'KB_EXT_PRODUCT_TYPE_CLS_POLICY'
|
||||
)
|
||||
GROUP BY policy.object_name
|
||||
)
|
||||
SELECT 'POC_2' AS owner,
|
||||
managed.object_name,
|
||||
managed.policy_name,
|
||||
database_policy.enable AS enabled,
|
||||
NVL(configured.configured_column_count, 0) AS configured_column_count,
|
||||
NVL(database_policy.applied_column_count, 0) AS applied_column_count,
|
||||
NVL(missing_columns.missing_column_count, 0) + NVL(extra_columns.extra_column_count, 0)
|
||||
AS mismatched_column_count,
|
||||
NVL(legacy_vpd_column_policy.legacy_vpd_column_policy_count, 0)
|
||||
AS legacy_vpd_column_policy_count
|
||||
FROM managed_policy managed
|
||||
LEFT JOIN configured ON configured.object_name = managed.object_name
|
||||
LEFT JOIN database_policy ON database_policy.object_name = managed.object_name
|
||||
LEFT JOIN missing_columns ON missing_columns.object_name = managed.object_name
|
||||
LEFT JOIN extra_columns ON extra_columns.object_name = managed.object_name
|
||||
LEFT JOIN legacy_vpd_column_policy
|
||||
ON legacy_vpd_column_policy.object_name = managed.object_name
|
||||
ORDER BY managed.object_name
|
||||
</select>
|
||||
|
||||
<select id="findColumnRule" resultType="com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule">
|
||||
SELECT c.column_id,
|
||||
c.object_id,
|
||||
o.owner,
|
||||
o.object_name,
|
||||
c.column_name,
|
||||
r.rule_id,
|
||||
r.rule_code,
|
||||
r.rule_name,
|
||||
r.template_code,
|
||||
r.enabled_yn AS rule_enabled_yn
|
||||
FROM cb_column_masking_rule link
|
||||
JOIN cb_protected_column c ON c.column_id = link.column_id
|
||||
JOIN cb_protected_object o ON o.object_id = c.object_id
|
||||
JOIN cb_masking_rule r ON r.rule_id = link.rule_id
|
||||
WHERE c.column_id = #{columnId}
|
||||
</select>
|
||||
|
||||
<insert id="upsertColumnRule">
|
||||
MERGE INTO cb_column_masking_rule dst
|
||||
USING (SELECT #{columnId} column_id, #{ruleId} rule_id FROM dual) src
|
||||
ON (dst.column_id = src.column_id)
|
||||
WHEN MATCHED THEN UPDATE SET dst.rule_id = src.rule_id, dst.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (column_id, rule_id, updated_at)
|
||||
VALUES (src.column_id, src.rule_id, SYSTIMESTAMP)
|
||||
</insert>
|
||||
|
||||
<delete id="deleteColumnRule">
|
||||
DELETE FROM cb_column_masking_rule
|
||||
WHERE column_id = #{columnId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteUserRulesForColumn">
|
||||
DELETE FROM cb_user_masking_rule
|
||||
WHERE column_id = #{columnId}
|
||||
</delete>
|
||||
|
||||
<select id="findUserRules" resultType="com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule">
|
||||
SELECT u.user_id,
|
||||
u.user_name AS username,
|
||||
c.column_id,
|
||||
o.owner,
|
||||
o.object_name,
|
||||
c.column_name,
|
||||
r.rule_name,
|
||||
r.template_code,
|
||||
assignment.decision,
|
||||
assignment.active_yn
|
||||
FROM cb_user_masking_rule assignment
|
||||
JOIN cb_app_user u ON u.user_id = assignment.user_id
|
||||
JOIN cb_protected_column c ON c.column_id = assignment.column_id
|
||||
JOIN cb_protected_object o ON o.object_id = c.object_id
|
||||
JOIN cb_column_masking_rule link ON link.column_id = c.column_id
|
||||
JOIN cb_masking_rule r ON r.rule_id = link.rule_id
|
||||
ORDER BY u.user_name, o.owner, o.object_name, c.column_name
|
||||
</select>
|
||||
|
||||
<select id="findUserRule" resultType="com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule">
|
||||
SELECT u.user_id,
|
||||
u.user_name AS username,
|
||||
c.column_id,
|
||||
o.owner,
|
||||
o.object_name,
|
||||
c.column_name,
|
||||
r.rule_name,
|
||||
r.template_code,
|
||||
assignment.decision,
|
||||
assignment.active_yn
|
||||
FROM cb_user_masking_rule assignment
|
||||
JOIN cb_app_user u ON u.user_id = assignment.user_id
|
||||
JOIN cb_protected_column c ON c.column_id = assignment.column_id
|
||||
JOIN cb_protected_object o ON o.object_id = c.object_id
|
||||
JOIN cb_column_masking_rule link ON link.column_id = c.column_id
|
||||
JOIN cb_masking_rule r ON r.rule_id = link.rule_id
|
||||
WHERE assignment.user_id = #{userId}
|
||||
AND assignment.column_id = #{columnId}
|
||||
</select>
|
||||
|
||||
<insert id="upsertUserRule">
|
||||
MERGE INTO cb_user_masking_rule dst
|
||||
USING (SELECT #{userId} user_id, #{columnId} column_id, #{decision} decision FROM dual) src
|
||||
ON (dst.user_id = src.user_id AND dst.column_id = src.column_id)
|
||||
WHEN MATCHED THEN UPDATE SET dst.decision = src.decision, dst.active_yn = 'Y', dst.updated_at = SYSTIMESTAMP
|
||||
WHEN NOT MATCHED THEN INSERT (user_id, column_id, decision, active_yn, updated_at)
|
||||
VALUES (src.user_id, src.column_id, src.decision, 'Y', SYSTIMESTAMP)
|
||||
</insert>
|
||||
|
||||
<delete id="deleteUserRule">
|
||||
DELETE FROM cb_user_masking_rule
|
||||
WHERE user_id = #{userId}
|
||||
AND column_id = #{columnId}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -100,13 +100,15 @@
|
||||
WHEN pr2.rule_type = 'CHANNEL_CONTRACT' THEN
|
||||
pr2.rule_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')'
|
||||
WHEN pr2.rule_type = 'OWN_CUSTOMER' THEN
|
||||
'EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS c '
|
||||
|| 'WHERE c.' || pr2.rule_column || ' = [CURRENT_ROW].' || pr2.rule_column || ' '
|
||||
|| 'AND c.FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||
'EXISTS (SELECT 1 FROM (SELECT c.' || pr2.rule_column || ' AS access_key '
|
||||
|| 'FROM POC_2.KB_CONTRACTS c '
|
||||
|| 'WHERE c.FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')) allowed_contract '
|
||||
|| 'WHERE allowed_contract.access_key = ' || pr2.rule_column || ')'
|
||||
WHEN pr2.rule_type = 'CHANNEL_CUSTOMER' THEN
|
||||
'EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS c '
|
||||
|| 'WHERE c.' || pr2.rule_column || ' = [CURRENT_ROW].' || pr2.rule_column || ' '
|
||||
|| 'AND c.FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL''))'
|
||||
'EXISTS (SELECT 1 FROM (SELECT c.' || pr2.rule_column || ' AS access_key '
|
||||
|| 'FROM POC_2.KB_CONTRACTS c '
|
||||
|| 'WHERE c.FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')) allowed_contract '
|
||||
|| 'WHERE allowed_contract.access_key = ' || pr2.rule_column || ')'
|
||||
WHEN pr2.rule_type = 'STATIC_SQL' THEN pr2.rule_value
|
||||
ELSE pr2.rule_column || ' ' || pr2.rule_type || ' ' || pr2.rule_value
|
||||
END,
|
||||
@@ -117,8 +119,8 @@
|
||||
) AS filter_preview,
|
||||
(
|
||||
SELECT CASE
|
||||
WHEN COUNT(*) = 0 THEN '모든 민감 컬럼 NULL 처리'
|
||||
ELSE 'NULL 제외: ' || LISTAGG(pc2.column_name, ', ') WITHIN GROUP (ORDER BY pc2.column_name)
|
||||
WHEN COUNT(*) = 0 THEN '컬럼 마스킹은 ASO에서 관리'
|
||||
ELSE '레거시 권한별 컬럼 예외: ' || LISTAGG(pc2.column_name, ', ') WITHIN GROUP (ORDER BY pc2.column_name)
|
||||
END
|
||||
FROM cb_permission_column pc2
|
||||
WHERE pc2.permission_id = p.perm_id
|
||||
|
||||
@@ -69,6 +69,47 @@
|
||||
ORDER BY column_id
|
||||
</select>
|
||||
|
||||
<select id="findColumnsByObjectIds" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn">
|
||||
SELECT column_id,
|
||||
object_id,
|
||||
column_name,
|
||||
sensitive_yn,
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM cb_protected_column
|
||||
WHERE object_id IN
|
||||
<foreach collection="objectIds" item="objectId" open="(" separator="," close=")">
|
||||
#{objectId,jdbcType=NUMERIC}
|
||||
</foreach>
|
||||
ORDER BY object_id, column_id
|
||||
</select>
|
||||
|
||||
<select id="findColumnById" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn">
|
||||
SELECT column_id,
|
||||
object_id,
|
||||
column_name,
|
||||
sensitive_yn,
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM cb_protected_column
|
||||
WHERE column_id = #{columnId}
|
||||
</select>
|
||||
|
||||
<select id="findColumnByObjectAndName" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn">
|
||||
SELECT column_id,
|
||||
object_id,
|
||||
column_name,
|
||||
sensitive_yn,
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM cb_protected_column
|
||||
WHERE object_id = #{objectId}
|
||||
AND column_name = UPPER(#{columnName,jdbcType=VARCHAR})
|
||||
</select>
|
||||
|
||||
<select id="nextObjectId" resultType="long">
|
||||
SELECT NVL(MAX(object_id), 0) + 1 FROM cb_protected_object
|
||||
</select>
|
||||
|
||||
@@ -295,6 +295,18 @@
|
||||
AND function_name = UPPER(#{functionName,jdbcType=VARCHAR})
|
||||
</select>
|
||||
|
||||
<select id="findPolicyDescriptions" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdDescriptionNote">
|
||||
SELECT object_owner || '.' || object_name || '|' || policy_name AS note_key,
|
||||
description
|
||||
FROM cb_vpd_policy_note
|
||||
</select>
|
||||
|
||||
<select id="findFilterDescriptions" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdDescriptionNote">
|
||||
SELECT function_owner || '|' || function_name AS note_key,
|
||||
description
|
||||
FROM cb_vpd_filter_note
|
||||
</select>
|
||||
|
||||
<update id="upsertPolicyDescription">
|
||||
MERGE INTO cb_vpd_policy_note dst
|
||||
USING (
|
||||
|
||||
@@ -2745,3 +2745,12 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.readonly-mutation-notice {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.readonly-mutation-disabled {
|
||||
opacity: .58;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,67 @@ document.body.addEventListener('htmx:responseError', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
const READ_ONLY_POST_ALLOWLIST = new Set([
|
||||
'/login',
|
||||
'/logout',
|
||||
'/probe',
|
||||
'/vector-knowledge/search',
|
||||
'/security-sql-scripts/explanation',
|
||||
'/mcp-chatbot',
|
||||
'/mcp-client-demo',
|
||||
'/mcp-reasoning',
|
||||
]);
|
||||
|
||||
function normalizeActionPath(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new URL(value, window.location.origin).pathname;
|
||||
} catch (_error) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function isReadOnlyMode() {
|
||||
return document.querySelector('meta[name="backoffice-read-only"]')?.content === 'true';
|
||||
}
|
||||
|
||||
function isFormMutation(form) {
|
||||
const method = (form.getAttribute('method') || 'get').toLowerCase();
|
||||
const hxPost = form.getAttribute('hx-post');
|
||||
if (hxPost) {
|
||||
return !READ_ONLY_POST_ALLOWLIST.has(normalizeActionPath(hxPost));
|
||||
}
|
||||
if (method !== 'post') {
|
||||
return false;
|
||||
}
|
||||
return !READ_ONLY_POST_ALLOWLIST.has(normalizeActionPath(form.getAttribute('action')));
|
||||
}
|
||||
|
||||
function disableReadOnlyMutationForms() {
|
||||
if (!isReadOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('form').forEach((form) => {
|
||||
if (!isFormMutation(form)) {
|
||||
return;
|
||||
}
|
||||
if (!form.previousElementSibling?.matches?.('[data-readonly-mutation-notice]')) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'alert alert-secondary readonly-mutation-notice';
|
||||
notice.setAttribute('data-readonly-mutation-notice', 'true');
|
||||
notice.textContent = '읽기 전용 계정은 이 변경 작업을 실행할 수 없습니다.';
|
||||
form.parentNode?.insertBefore(notice, form);
|
||||
}
|
||||
form.classList.add('readonly-mutation-disabled');
|
||||
form.setAttribute('aria-disabled', 'true');
|
||||
form.querySelectorAll('input:not([type="hidden"]), select, textarea, button').forEach((control) => {
|
||||
control.disabled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilterToggle(button, target) {
|
||||
if (!button || !target) {
|
||||
return;
|
||||
@@ -324,60 +385,6 @@ function renderRuleColumnOptions(columns) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderMaskableColumnOptions(option) {
|
||||
const list = document.querySelector('[data-maskable-column-list]');
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
const columns = (option?.dataset.maskableColumns || '').split(',').filter(Boolean);
|
||||
const labels = (option?.dataset.maskableLabels || '').split('|').filter(Boolean);
|
||||
if (!columns.length) {
|
||||
list.innerHTML = '<span class="text-muted small">선택한 객체에 권한별로 허용할 마스킹 컬럼이 없습니다.</span>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = columns.map((column, index) => (
|
||||
`<button class="btn btn-sm btn-outline-secondary question-preset" type="button" data-mask-column="${column}">`
|
||||
+ `${escapeHtml(labels[index] || column)}</button>`
|
||||
)).join('');
|
||||
list.querySelectorAll('[data-mask-column]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const input = document.querySelector('input[name="visibleColumns"]');
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
const current = input.value.split(',').map((value) => value.trim()).filter(Boolean);
|
||||
const column = button.dataset.maskColumn;
|
||||
if (!current.includes(column)) {
|
||||
current.push(column);
|
||||
}
|
||||
input.value = current.join(', ');
|
||||
renderSelectedVisibleColumns();
|
||||
updatePermissionWizardPreview(document);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderSelectedVisibleColumns() {
|
||||
const input = document.querySelector('input[name="visibleColumns"]');
|
||||
const list = document.querySelector('[data-selected-visible-columns]');
|
||||
if (!input || !list) {
|
||||
return;
|
||||
}
|
||||
const columns = input.value.split(',').map((value) => value.trim().toUpperCase()).filter(Boolean);
|
||||
list.innerHTML = columns.map((column) => (
|
||||
`<button type="button" class="badge text-bg-secondary border-0 me-1 mb-1" data-remove-visible-column="${column}">`
|
||||
+ `${escapeHtml(column)} ×</button>`
|
||||
)).join('');
|
||||
list.querySelectorAll('[data-remove-visible-column]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const remove = button.dataset.removeVisibleColumn;
|
||||
input.value = columns.filter((column) => column !== remove).join(', ');
|
||||
renderSelectedVisibleColumns();
|
||||
updatePermissionWizardPreview(document);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncRuleTypeHints(root = document) {
|
||||
root.querySelectorAll('.rule-row').forEach((row) => {
|
||||
const typeSelect = row.querySelector('.rule-type-select');
|
||||
@@ -440,7 +447,6 @@ async function syncRuleColumnOptions() {
|
||||
renderRuleColumnOptions(columns);
|
||||
updatePermissionWizardPreview(document);
|
||||
};
|
||||
renderMaskableColumnOptions(selectedOption);
|
||||
applyColumns(fallbackColumns);
|
||||
try {
|
||||
const response = await fetch(`/permissions/object-columns?objectRef=${encodeURIComponent(objectSelect.value)}`);
|
||||
@@ -700,6 +706,54 @@ function sqlLiteral(value) {
|
||||
return `'${String(value || '').replaceAll("'", "''")}'`;
|
||||
}
|
||||
|
||||
function permissionRuleBusinessLabel(type, column, value) {
|
||||
const displayColumn = column || {
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
TOKEN_SUBJECT: 'USER_ID',
|
||||
OWN_CONTRACT: 'FC_ID',
|
||||
CHANNEL_CONTRACT: 'FC_CHANNEL',
|
||||
OWN_CUSTOMER: 'CUST_ID/CONTRACT_NO',
|
||||
CHANNEL_CUSTOMER: 'CUST_ID/CONTRACT_NO',
|
||||
STATIC_SQL: '',
|
||||
ALL: ''
|
||||
}[type] || '선택 컬럼';
|
||||
if (type === 'ALL') {
|
||||
return '전체 행';
|
||||
}
|
||||
if (type === 'TOKEN_SUBJECT') {
|
||||
return `토큰 이해관계자 본인 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'OWN_CONTRACT') {
|
||||
return `담당 설계사 본인 계약 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'CHANNEL_CONTRACT') {
|
||||
return `토큰 사용자 채널 계약 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'OWN_CUSTOMER') {
|
||||
return `담당 설계사 본인 계약에 연결된 고객/청구/외부보유 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'CHANNEL_CUSTOMER') {
|
||||
return `토큰 사용자 채널 계약에 연결된 고객/청구/외부보유 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'STATIC_SQL') {
|
||||
return `정적 SQL 조건: ${value || '조건식 미입력'}`;
|
||||
}
|
||||
if (type === 'MY_DEPT') {
|
||||
return `내 부서 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'SELF') {
|
||||
return `내 사번/소유자 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'TAG') {
|
||||
return `태그 조건 ${displayColumn} contains ${value || '-'}`;
|
||||
}
|
||||
return [displayColumn, type, value].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function collectWizardRules(root) {
|
||||
return Array.from(root.querySelectorAll('.rule-row')).map((row) => {
|
||||
const column = row.querySelector('[name="ruleColumn"]')?.value || '';
|
||||
@@ -721,25 +775,7 @@ function collectWizardRules(root) {
|
||||
STATIC_SQL: '',
|
||||
ALL: ''
|
||||
}[type] || '';
|
||||
if (type === 'ALL') {
|
||||
return 'ALL';
|
||||
}
|
||||
if (type === 'STATIC_SQL') {
|
||||
return `정적 SQL: ${value}`;
|
||||
}
|
||||
if (['MY_DEPT', 'SELF'].includes(type)) {
|
||||
return `${displayColumn} ${type}`;
|
||||
}
|
||||
if (['STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type)) {
|
||||
return `${displayColumn} ${type} 역할=${value}`;
|
||||
}
|
||||
if (['TOKEN_SUBJECT', 'OWN_CONTRACT', 'CHANNEL_CONTRACT', 'OWN_CUSTOMER', 'CHANNEL_CUSTOMER'].includes(type)) {
|
||||
return `${displayColumn} ${type}`;
|
||||
}
|
||||
if (type === 'TAG') {
|
||||
return `${displayColumn} TAG ${value.toUpperCase()}`;
|
||||
}
|
||||
return [displayColumn, type, value].filter(Boolean).join(' ');
|
||||
return permissionRuleBusinessLabel(type, displayColumn, value);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -821,7 +857,6 @@ function updatePermissionWizardPreview(root = document) {
|
||||
const roleSelect = wizard.querySelector('[name="roleId"]');
|
||||
const objectSelect = wizard.querySelector('[name="objectRef"]');
|
||||
const effectSelect = wizard.querySelector('[name="permissionEffect"]');
|
||||
const visibleColumns = wizard.querySelector('[name="visibleColumns"]')?.value.trim() || '';
|
||||
const roleOption = selectedOption(roleSelect);
|
||||
const objectOption = selectedOption(objectSelect);
|
||||
const effect = effectSelect?.value || 'ALLOW';
|
||||
@@ -837,20 +872,15 @@ function updatePermissionWizardPreview(root = document) {
|
||||
const groupUsers = splitList(roleOption?.dataset.groupUsers || '');
|
||||
const objectColumns = splitList(objectOption?.dataset.columns || '', ',');
|
||||
const maskableColumns = splitList(objectOption?.dataset.maskableColumns || '', ',');
|
||||
const visibleColumnList = splitList(visibleColumns, ',').map((column) => column.toUpperCase());
|
||||
const nullColumns = maskableColumns.filter((column) => !visibleColumnList.includes(column.toUpperCase()));
|
||||
const rowPolicy = effect === 'DENY'
|
||||
? `거부 규칙: ${ruleText}`
|
||||
: `허용 규칙: ${ruleText}`;
|
||||
const predicateText = effect === 'DENY'
|
||||
? (rules.includes('ALL') ? 'DENY ALL: 1 = 0' : `DENY 후보: NOT (${predicates.join(' AND ') || '조건 없음'})`)
|
||||
: `ALLOW 후보: ${predicates.join(' AND ') || '조건 없음'}`;
|
||||
const columnPolicy = visibleColumns
|
||||
? `이 권한에서 원문 표시 허용: ${visibleColumns}`
|
||||
: '마스킹 대상 컬럼은 기본 정책대로 NULL/마스킹 처리';
|
||||
const nullPolicy = !maskableColumns.length
|
||||
? '마스킹 대상 컬럼 없음'
|
||||
: (nullColumns.length ? `NULL 처리: ${nullColumns.join(', ')}` : '선택한 마스킹 컬럼 모두 원문 표시 허용');
|
||||
const columnPolicy = maskableColumns.length
|
||||
? `ASO 마스킹 후보: ${maskableColumns.join(', ')}. 실제 원문/마스킹은 컬럼 마스킹 화면에서 설정합니다.`
|
||||
: '이 행 접근 규칙은 행만 제어합니다. 컬럼 원문/마스킹은 컬럼 마스킹에서 관리합니다.';
|
||||
const affectedPrincipals = `직접 사용자 ${directUsers.length}명 / 그룹 ${groups.length}개 / 그룹 상속 사용자 ${groupUsers.length}명`;
|
||||
const readiness = !hasRole
|
||||
? '역할을 선택하세요'
|
||||
@@ -883,7 +913,6 @@ function updatePermissionWizardPreview(root = document) {
|
||||
setWizardPreview(wizard, 'rowPolicy', rowPolicy);
|
||||
setWizardPreview(wizard, 'predicatePreview', predicateText);
|
||||
setWizardPreview(wizard, 'columnPolicy', columnPolicy);
|
||||
setWizardPreview(wizard, 'nullPolicy', nullPolicy);
|
||||
setWizardPreview(wizard, 'saveGuard', hasRole && hasObject ? saveGuard : '역할과 보호 객체를 선택하면 저장 영향을 계산합니다.');
|
||||
}
|
||||
|
||||
@@ -1185,12 +1214,72 @@ function initVpdTargetFilters() {
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function initMaskingExceptionPreview() {
|
||||
const preview = document.querySelector('[data-masking-exception-preview]');
|
||||
const columnSelect = document.querySelector('select[name="columnId"]');
|
||||
const userSelect = document.querySelector('select[name="userId"]');
|
||||
if (!preview || !columnSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const empty = preview.querySelector('[data-masking-preview-empty]');
|
||||
const details = preview.querySelector('[data-masking-preview-details]');
|
||||
const target = preview.querySelector('[data-masking-preview-target]');
|
||||
const masked = preview.querySelector('[data-masking-preview-masked]');
|
||||
const unmasked = preview.querySelector('[data-masking-preview-unmasked]');
|
||||
const context = preview.querySelector('[data-masking-preview-context]');
|
||||
|
||||
const update = () => {
|
||||
const option = columnSelect.options[columnSelect.selectedIndex];
|
||||
if (!option?.value) {
|
||||
if (empty) {
|
||||
empty.hidden = false;
|
||||
}
|
||||
if (details) {
|
||||
details.hidden = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const selectedUser = userSelect?.options[userSelect.selectedIndex];
|
||||
const userLabel = selectedUser?.dataset.userLabel || '선택한 사용자';
|
||||
const columnTarget = option.dataset.target || option.textContent?.trim() || '선택한 컬럼';
|
||||
const template = option.dataset.template || '컬럼 마스킹 규칙';
|
||||
const result = option.dataset.result || '마스킹된 값';
|
||||
const asoFunction = option.dataset.asoFunction || 'DBMS_REDACT';
|
||||
const contextName = option.dataset.context || 'MR_<column_id>';
|
||||
|
||||
if (empty) {
|
||||
empty.hidden = true;
|
||||
}
|
||||
if (details) {
|
||||
details.hidden = false;
|
||||
}
|
||||
if (target) {
|
||||
target.textContent = `${columnTarget} · ${template} (${asoFunction})`;
|
||||
}
|
||||
if (masked) {
|
||||
masked.textContent = `${columnTarget} 값은 ${result}로 반환됩니다.`;
|
||||
}
|
||||
if (unmasked) {
|
||||
unmasked.textContent = `${userLabel}은(는) 행 접근 정책이 허용한 행에서 원래 ${columnTarget} 값을 봅니다.`;
|
||||
}
|
||||
if (context) {
|
||||
context.textContent = `CB_AGENT_CTX.${contextName} = Y이면 원문, 기본값 또는 N이면 ${asoFunction}으로 마스킹합니다.`;
|
||||
}
|
||||
};
|
||||
|
||||
columnSelect.addEventListener('change', update);
|
||||
userSelect?.addEventListener('change', update);
|
||||
update();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initPersistentMenus();
|
||||
initQuestionPresets();
|
||||
initCopyButtons();
|
||||
renderMarkdownViews();
|
||||
initVpdTargetFilters();
|
||||
initMaskingExceptionPreview();
|
||||
const master = document.getElementById('userRoleMaster');
|
||||
if (master) {
|
||||
master.addEventListener('change', filterUserRoleDetail);
|
||||
@@ -1219,8 +1308,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
syncRuleTypeHints();
|
||||
document.querySelector('input[name="visibleColumns"]')?.addEventListener('input', renderSelectedVisibleColumns);
|
||||
renderSelectedVisibleColumns();
|
||||
const catalog = document.getElementById('objectCatalogSelect');
|
||||
if (catalog) {
|
||||
catalog.addEventListener('change', syncObjectCatalogSelection);
|
||||
@@ -1261,6 +1348,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
updatePermissionWizardPreview(document);
|
||||
disableReadOnlyMutationForms();
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:beforeRequest', (event) => {
|
||||
@@ -1284,4 +1372,5 @@ document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
button.classList.add('active');
|
||||
}
|
||||
renderMarkdownViews(event.detail.target || document);
|
||||
disableReadOnlyMutationForms();
|
||||
});
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('VPD 권한 운영')}"></head>
|
||||
<head th:replace="~{fragments/layout :: head('데이터 접근 제어')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4 dashboard-page">
|
||||
<header class="dashboard-hero">
|
||||
<div>
|
||||
<span class="architecture-kicker">VPD 권한 운영</span>
|
||||
<h1>권한 운영 흐름</h1>
|
||||
<span class="architecture-kicker">데이터 접근 제어</span>
|
||||
<h1>행 접근과 컬럼 마스킹 운영 흐름</h1>
|
||||
<p>업무 사용자와 데이터 접근 기준을 관리하고, DB가 적용한 결과까지 확인합니다.</p>
|
||||
</div>
|
||||
<div class="dashboard-hero-actions" aria-label="주요 작업">
|
||||
<a class="btn rw-btn-primary" href="/probe">접근 검증</a>
|
||||
<a class="btn rw-btn-secondary" href="/permissions">접근 규칙</a>
|
||||
<a class="btn rw-btn-secondary" href="/permissions">행 접근 규칙</a>
|
||||
</div>
|
||||
<details class="explanation-details dashboard-explanation">
|
||||
<summary>도움말: 메뉴와 권한 적용 구조 보기</summary>
|
||||
<div class="product-help-overview">
|
||||
<section class="product-help-section">
|
||||
<h2>왜 권한 테이블을 따로 관리하나요?</h2>
|
||||
<p>DB 연결은 VPD/DDS 실행 계정 하나로 유지하되, 실제 업무 사용자는 <code>CB_APP_USER</code>와 역할·권한 테이블에서 찾습니다. 요청마다 토큰이 사용자를 식별하고 VPD가 그 사용자의 권한만 조건으로 계산하므로, 같은 실행 계정으로도 사용자마다 다른 행과 컬럼을 안전하게 반환할 수 있습니다.</p>
|
||||
<p>DB 연결은 공용 실행 계정 하나로 유지하되, 실제 업무 사용자는 <code>CB_APP_USER</code>와 역할·권한 테이블에서 찾습니다. 요청마다 토큰이 사용자를 식별하고 행 접근 정책이 그 사용자의 행 조건을 계산하며, 컬럼 원문/마스킹은 ASO/Data Redaction 정책이 별도로 판단합니다.</p>
|
||||
</section>
|
||||
|
||||
<section class="product-help-section">
|
||||
<h2>메뉴 안내</h2>
|
||||
<dl class="menu-role-grid">
|
||||
<div><dt>권한 관리</dt><dd>사용자·그룹·역할과 접근 규칙을 정하고, 최종 권한을 확인합니다.</dd></div>
|
||||
<div><dt>접근 제어 관리</dt><dd>사용자·그룹·역할과 행 접근 규칙, 컬럼 마스킹 설정을 정하고 최종 적용 결과를 확인합니다.</dd></div>
|
||||
<div><dt>보호·검증</dt><dd>보호 정책을 연결하고 토큰으로 실제 DB 조회 결과를 검증합니다.</dd></div>
|
||||
<div><dt>연동 도구</dt><dd>조회 대상, ORDS 연동, 지식 검색과 MCP 연결을 관리합니다.</dd></div>
|
||||
<div><dt>운영</dt><dd>현재 연결과 실행 상태를 확인합니다.</dd></div>
|
||||
@@ -38,8 +38,8 @@
|
||||
<p>권한을 먼저 만들고 보호 대상을 연결한 뒤, 실제 사용자 토큰으로 결과를 검증합니다. 권한은 토큰에 복사되지 않아 이후 변경도 다음 요청부터 반영됩니다.</p>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram setup-flow-diagram" viewBox="0 0 1120 245" role="img" aria-labelledby="setup-flow-title setup-flow-desc">
|
||||
<title id="setup-flow-title">VPD 권한 설정과 검증 순서</title>
|
||||
<desc id="setup-flow-desc">사용자와 그룹, 역할, 접근 규칙, 보호 대상을 차례로 설정하고 유효 권한과 실제 조회 결과를 검증하는 흐름</desc>
|
||||
<title id="setup-flow-title">데이터 접근 제어 설정과 검증 순서</title>
|
||||
<desc id="setup-flow-desc">사용자와 그룹, 역할, 행 접근 규칙, 컬럼 마스킹, 보호 대상을 차례로 설정하고 실제 조회 결과를 검증하는 흐름</desc>
|
||||
<defs><marker id="setup-flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
|
||||
<path class="diagram-link" marker-end="url(#setup-flow-arrow)" d="M205 100 H238"/>
|
||||
<path class="diagram-link" marker-end="url(#setup-flow-arrow)" d="M415 100 H448"/>
|
||||
@@ -47,11 +47,11 @@
|
||||
<path class="diagram-link" marker-end="url(#setup-flow-arrow)" d="M835 100 H868"/>
|
||||
<g class="diagram-node diagram-node-step"><rect x="30" y="52" width="175" height="96" rx="12"/><circle cx="57" cy="77" r="15"/><text x="57" y="82" class="diagram-step-number">1</text><text x="117" y="85" class="diagram-node-title">사용자·그룹</text><text x="117" y="113" class="diagram-node-detail">업무 대상을 등록</text></g>
|
||||
<g class="diagram-node diagram-node-step"><rect x="240" y="52" width="175" height="96" rx="12"/><circle cx="267" cy="77" r="15"/><text x="267" y="82" class="diagram-step-number">2</text><text x="327" y="85" class="diagram-node-title">역할</text><text x="327" y="113" class="diagram-node-detail">직접·그룹 역할 부여</text></g>
|
||||
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="450" y="52" width="175" height="96" rx="12"/><circle cx="477" cy="77" r="15"/><text x="477" y="82" class="diagram-step-number">3</text><text x="537" y="85" class="diagram-node-title">접근 규칙</text><text x="537" y="113" class="diagram-node-detail">객체·행·컬럼 설정</text></g>
|
||||
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="660" y="52" width="175" height="96" rx="12"/><circle cx="687" cy="77" r="15"/><text x="687" y="82" class="diagram-step-number">4</text><text x="747" y="85" class="diagram-node-title">보호·연결</text><text x="747" y="113" class="diagram-node-detail">VPD·ORDS 대상 확인</text></g>
|
||||
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="450" y="52" width="175" height="96" rx="12"/><circle cx="477" cy="77" r="15"/><text x="477" y="82" class="diagram-step-number">3</text><text x="537" y="85" class="diagram-node-title">행 접근 규칙</text><text x="537" y="113" class="diagram-node-detail">객체·행 조건 설정</text></g>
|
||||
<g class="diagram-node diagram-node-step diagram-node-accent"><rect x="660" y="52" width="175" height="96" rx="12"/><circle cx="687" cy="77" r="15"/><text x="687" y="82" class="diagram-step-number">4</text><text x="747" y="85" class="diagram-node-title">보호·연결</text><text x="747" y="113" class="diagram-node-detail">정책·ORDS 대상 확인</text></g>
|
||||
<g class="diagram-node diagram-node-step diagram-node-data"><rect x="870" y="52" width="220" height="96" rx="12"/><circle cx="897" cy="77" r="15"/><text x="897" y="82" class="diagram-step-number">5</text><text x="977" y="85" class="diagram-node-title">유효 권한·접근 검증</text><text x="977" y="113" class="diagram-node-detail">토큰으로 실제 결과 확인</text></g>
|
||||
<path class="setup-flow-return" d="M980 166 V192 H117 V166"/>
|
||||
<text x="548" y="218" class="setup-flow-note">규칙 변경은 다음 요청의 VPD 조건 계산에 동적으로 반영됩니다.</text>
|
||||
<text x="548" y="218" class="setup-flow-note">규칙 변경은 다음 요청의 행 조건 계산에 동적으로 반영됩니다.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
@@ -60,8 +60,8 @@
|
||||
<h2>요청마다 권한이 적용되는 흐름</h2>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram" viewBox="0 0 1120 270" role="img" aria-labelledby="vpd-flow-title vpd-flow-desc">
|
||||
<title id="vpd-flow-title">VPD 권한 적용 흐름</title>
|
||||
<desc id="vpd-flow-desc">Bearer Token 요청이 공용 실행 계정과 사용자 컨텍스트를 거쳐 권한 테이블에서 계산한 VPD 조건으로 보호 데이터를 조회하는 흐름</desc>
|
||||
<title id="vpd-flow-title">행 접근 적용 흐름</title>
|
||||
<desc id="vpd-flow-desc">Bearer Token 요청이 공용 실행 계정과 사용자 컨텍스트를 거쳐 권한 테이블에서 계산한 행 조건으로 보호 데이터를 조회하는 흐름</desc>
|
||||
<defs><marker id="flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M190 92 H245"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M395 92 H450"/>
|
||||
@@ -71,8 +71,8 @@
|
||||
<g class="diagram-node"><rect x="30" y="48" width="160" height="88" rx="12"/><text x="110" y="81" class="diagram-node-title">요청·토큰</text><text x="110" y="108" class="diagram-node-detail">Bearer Token</text></g>
|
||||
<g class="diagram-node"><rect x="245" y="48" width="150" height="88" rx="12"/><text x="320" y="78" class="diagram-node-title">공용 실행 계정</text><text x="320" y="105" class="diagram-node-detail">CB_ORDS</text><text x="320" y="123" class="diagram-node-note">한 개의 DB 연결</text></g>
|
||||
<g class="diagram-node"><rect x="450" y="48" width="150" height="88" rx="12"/><text x="525" y="78" class="diagram-node-title">사용자 컨텍스트</text><text x="525" y="105" class="diagram-node-detail">CB_AGENT_CTX</text><text x="525" y="123" class="diagram-node-note">사용자별로 설정</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="655" y="48" width="150" height="88" rx="12"/><text x="730" y="78" class="diagram-node-title">VPD 조건 계산</text><text x="730" y="105" class="diagram-node-detail">ALLOW · DENY · 행 · 열</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="860" y="48" width="210" height="88" rx="12"/><text x="965" y="78" class="diagram-node-title">보호 데이터 조회</text><text x="965" y="105" class="diagram-node-detail">허용된 행·컬럼만 반환</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="655" y="48" width="150" height="88" rx="12"/><text x="730" y="78" class="diagram-node-title">행 조건 계산</text><text x="730" y="105" class="diagram-node-detail">ALLOW · DENY · predicate</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="860" y="48" width="210" height="88" rx="12"/><text x="965" y="78" class="diagram-node-title">보호 데이터 조회</text><text x="965" y="105" class="diagram-node-detail">허용된 행 반환</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="570" y="180" width="300" height="62" rx="12"/><text x="720" y="207" class="diagram-node-title">사용자 · 역할 · 권한 테이블</text><text x="720" y="229" class="diagram-node-detail">요청 시 동적으로 조회</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@
|
||||
<h2>권한 데이터 모델</h2>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="permission-erd-diagram" viewBox="0 0 1180 355" role="img" aria-labelledby="erd-title erd-desc">
|
||||
<title id="erd-title">VPD 권한 ERD</title>
|
||||
<title id="erd-title">접근 제어 ERD</title>
|
||||
<desc id="erd-desc">사용자와 그룹에서 역할을 얻고 역할에서 권한과 권한 규칙을 거쳐 보호 대상으로 연결되는 데이터 모델</desc>
|
||||
<defs><marker id="erd-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z"/></marker></defs>
|
||||
<path class="diagram-link" marker-end="url(#erd-arrow)" d="M175 90 H230"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M390 90 H445"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M605 90 H660"/><path class="diagram-link" marker-end="url(#erd-arrow)" d="M820 90 H875"/>
|
||||
@@ -92,7 +92,7 @@
|
||||
<g class="erd-entity"><rect x="230" y="50" width="160" height="80" rx="10"/><text x="310" y="80">CB_USER_ROLE</text><text x="310" y="105">직접 역할 연결</text></g>
|
||||
<g class="erd-entity"><rect x="445" y="50" width="160" height="80" rx="10"/><text x="525" y="80">CB_APP_ROLE</text><text x="525" y="105">역할</text></g>
|
||||
<g class="erd-entity"><rect x="660" y="50" width="160" height="80" rx="10"/><text x="740" y="80">CB_PERMISSION</text><text x="740" y="105">객체 접근</text></g>
|
||||
<g class="erd-entity"><rect x="875" y="50" width="160" height="80" rx="10"/><text x="955" y="80">CB_PERMISSION_RULE</text><text x="955" y="105">행·열 조건</text></g>
|
||||
<g class="erd-entity"><rect x="875" y="50" width="160" height="80" rx="10"/><text x="955" y="80">CB_PERMISSION_RULE</text><text x="955" y="105">행 조건 매핑</text></g>
|
||||
<g class="erd-entity erd-entity-accent"><rect x="1045" y="50" width="115" height="80" rx="10"/><text x="1102" y="80">보호 대상</text><text x="1102" y="105">TABLE / VIEW</text></g>
|
||||
<g class="erd-entity"><rect x="25" y="205" width="150" height="80" rx="10"/><text x="100" y="235">CB_USER_GROUP</text><text x="100" y="260">그룹 소속</text></g>
|
||||
<g class="erd-entity"><rect x="230" y="205" width="160" height="80" rx="10"/><text x="310" y="235">CB_GROUP</text><text x="310" y="260">사용자 그룹</text></g>
|
||||
@@ -119,10 +119,10 @@
|
||||
<section class="dashboard-menu-flow" aria-labelledby="menu-flow-title">
|
||||
<div class="dashboard-section-heading">
|
||||
<span class="architecture-kicker">업무 흐름</span>
|
||||
<h2 id="menu-flow-title">설정은 권한 관리에서, 적용 확인은 접근 검증에서</h2>
|
||||
<h2 id="menu-flow-title">설정은 접근 제어에서, 적용 확인은 접근 검증에서</h2>
|
||||
</div>
|
||||
<div class="dashboard-flow-grid">
|
||||
<a class="dashboard-flow-card" href="/permissions"><strong>권한 관리</strong><span>사용자·역할·접근 규칙</span></a>
|
||||
<a class="dashboard-flow-card" href="/permissions"><strong>행 접근 규칙</strong><span>역할별 객체·행 조건</span></a>
|
||||
<a class="dashboard-flow-card" href="/vpd-policies"><strong>보호·검증</strong><span>보호 연결·검증 세션·접근 확인</span></a>
|
||||
<a class="dashboard-flow-card" href="/objects"><strong>연동 도구</strong><span>조회 대상·지식 검색·MCP 연동</span></a>
|
||||
<a class="dashboard-flow-card" href="/operation-status"><strong>운영</strong><span>상태와 실행 결과 확인</span></a>
|
||||
@@ -134,7 +134,7 @@
|
||||
<h2>바로 시작</h2>
|
||||
<div class="dashboard-action-grid">
|
||||
<a class="dashboard-action" href="/permissions">
|
||||
<span><strong>접근 규칙</strong><small>역할별 객체·행·컬럼 접근을 설정합니다.</small></span>
|
||||
<span><strong>행 접근 규칙</strong><small>역할별 객체·행 조건을 설정합니다. 컬럼은 ASO 마스킹에서 관리합니다.</small></span>
|
||||
<span class="dashboard-action-arrow" aria-hidden="true">→</span>
|
||||
</a>
|
||||
<a class="dashboard-action dashboard-action-emphasis" href="/probe">
|
||||
@@ -161,7 +161,7 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>현재 검증 가능한 데이터</h2>
|
||||
<p class="section-subtitle">권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
|
||||
<p class="section-subtitle">행 접근 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-primary" href="/probe">접근 검증</a>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@
|
||||
<td><code th:text="${object.ordsPath()}">path</code></td>
|
||||
<td><span class="badge text-bg-success">사용 가능</span></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(objects)}"><td colspan="3" class="text-muted">아직 검증할 보호 객체가 없습니다. 권한 규칙에서 객체 권한을 먼저 등록하세요.</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(objects)}"><td colspan="3" class="text-muted">아직 검증할 보호 객체가 없습니다. 행 접근 규칙에서 객체 접근을 먼저 등록하세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>사용자별 접근 확인</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>직접 역할과 그룹 상속 역할을 합쳐 최종 권한을 계산합니다. 여기의 결과는 토큰을 발급해 ORDS/VPD 조회를 검증하기 전 확인하는 설계 근거입니다.</p>
|
||||
<p>직접 역할과 그룹 상속 역할을 합쳐 최종 권한을 계산합니다. 여기의 결과는 토큰을 발급해 ORDS 행 접근 조회를 검증하기 전 확인하는 설계 근거입니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<head th:fragment="head(title)">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${title == 'VPD 권한 운영' ? title : title + ' · VPD 권한 운영'}">VPD 권한 운영</title>
|
||||
<meta name="backoffice-can-mutate" th:content="${canMutate}">
|
||||
<meta name="backoffice-read-only" th:content="${readOnlyMode}">
|
||||
<title th:text="${title == '데이터 접근 제어' ? title : title + ' · 데이터 접근 제어'}">데이터 접근 제어</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/app.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
@@ -13,15 +15,15 @@
|
||||
<body>
|
||||
<nav th:fragment="nav" class="navbar rw-nav navbar-expand-lg" data-product-nav>
|
||||
<div class="container rw-nav-top">
|
||||
<a class="navbar-brand" href="/">VPD 권한 운영</a>
|
||||
<a class="navbar-brand" href="/">데이터 접근 제어</a>
|
||||
<div class="rw-menu" aria-label="주요 메뉴">
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">권한 관리</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">접근 제어</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="protection" aria-controls="submenu-protection" aria-expanded="false">보호·검증</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="integration" aria-controls="submenu-integration" aria-expanded="false">연동 도구</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="operations" aria-controls="submenu-operations" aria-expanded="false">운영</button>
|
||||
</div>
|
||||
<div class="rw-nav-utility ms-auto">
|
||||
<a class="rw-quick-link" href="/permissions">권한 설정</a>
|
||||
<a class="rw-quick-link" href="/permissions">행 접근 규칙</a>
|
||||
<a class="rw-quick-link" href="/tokens">토큰 발급</a>
|
||||
<button class="rw-admin-trigger" type="button" data-submenu-trigger="admin" aria-controls="submenu-admin" aria-expanded="false">관리자</button>
|
||||
<form method="post" action="/logout">
|
||||
@@ -32,15 +34,17 @@
|
||||
</div>
|
||||
<div class="rw-submenu-bar" data-submenu-bar hidden>
|
||||
<div class="container">
|
||||
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="권한 관리 메뉴" hidden>
|
||||
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="접근 제어 메뉴" hidden>
|
||||
<a class="nav-link" href="/users">사용자 관리</a>
|
||||
<a class="nav-link" href="/groups">그룹 관리</a>
|
||||
<a class="nav-link" href="/roles">역할 관리</a>
|
||||
<a class="nav-link" href="/permissions">권한 설정</a>
|
||||
<a class="nav-link" href="/permissions">행 접근 규칙</a>
|
||||
<a class="nav-link" href="/user-masking-rules">컬럼 원문 표시 허용</a>
|
||||
<a class="nav-link" href="/effective-matrix">권한 현황</a>
|
||||
</div>
|
||||
<div id="submenu-protection" class="rw-submenu" data-submenu-panel="protection" role="navigation" aria-label="보호 및 검증 메뉴" hidden>
|
||||
<a class="nav-link" href="/vpd-policies">보호 상태</a>
|
||||
<a class="nav-link" href="/masking-rules">컬럼 마스킹</a>
|
||||
<a class="nav-link" href="/tokens">토큰 발급</a>
|
||||
<a class="nav-link" href="/probe">접근 검증</a>
|
||||
</div>
|
||||
@@ -58,6 +62,9 @@
|
||||
<a class="nav-link" href="/operation-status">운영 현황</a>
|
||||
</div>
|
||||
<div id="submenu-admin" class="rw-submenu" data-submenu-panel="admin" role="navigation" aria-label="관리자 메뉴" hidden>
|
||||
<a class="nav-link" href="/vpd-filter-runtime">행 접근 필터 구조</a>
|
||||
<a class="nav-link" href="/schema-metadata">DB 메타데이터</a>
|
||||
<a class="nav-link" href="/security-sql-scripts">보안 SQL 스크립트</a>
|
||||
<a class="nav-link" href="/vpd-filter-policies">고급 접근 조건</a>
|
||||
<a class="nav-link" href="/settings">시스템 설정</a>
|
||||
<a class="nav-link" href="/settings/database">DB 준비 상태</a>
|
||||
|
||||
@@ -48,24 +48,41 @@
|
||||
|
||||
<section class="result-section">
|
||||
<div class="section-heading compact-heading">
|
||||
<h3>VPD가 적용된 실제 결과</h3>
|
||||
<h3>행 접근 정책이 적용된 실제 결과</h3>
|
||||
<span th:if="${selectedObject}"><code th:text="${selectedObject.displayName()}">ADMIN.OBJECT</code></span>
|
||||
</div>
|
||||
<div class="result-metrics">
|
||||
<div><span>보이는 행</span><strong th:text="${result.rowCount()}">0</strong></div>
|
||||
<div><span>행 접근 후 남은 행</span><strong th:text="${result.rowCount()}">0</strong></div>
|
||||
<div>
|
||||
<span>NULL/마스킹으로 확인된 컬럼</span>
|
||||
<span>ASO 마스킹 확인 컬럼</span>
|
||||
<strong th:if="${!#lists.isEmpty(result.maskedColumns())}" th:text="${#strings.listJoin(result.maskedColumns(), ', ')}">column</strong>
|
||||
<strong th:if="${#lists.isEmpty(result.maskedColumns())}">없음</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="effective-preview mt-3">
|
||||
<dl>
|
||||
<div>
|
||||
<dt>행 접근 판단</dt>
|
||||
<dd th:text="${result.rowCount() == 0 ? '토큰 context와 접근 규칙 기준으로 반환 가능한 행이 없습니다.' : '토큰 context와 접근 규칙 기준으로 반환 가능한 행이 있습니다.'}">반환 가능</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>ASO 컬럼 표시 판단</dt>
|
||||
<dd th:if="${!#lists.isEmpty(result.maskedColumns())}" th:text="${'마스킹 또는 NULL로 반환된 민감 컬럼: ' + #strings.listJoin(result.maskedColumns(), ', ')}">마스킹 컬럼</dd>
|
||||
<dd th:if="${#lists.isEmpty(result.maskedColumns())}">응답 기준으로 전체 NULL/마스킹 처리된 민감 컬럼을 찾지 못했습니다. 원문 허용이거나 해당 컬럼이 응답에 없을 수 있습니다.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>확인 기준</dt>
|
||||
<dd>행 개수는 행 접근 정책 결과이고, 컬럼 값은 ASO/Data Redaction 결과입니다. 두 판단은 별도로 봐야 합니다.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="vector-result-panel mt-3"
|
||||
th:if="${vectorSearch and !#lists.isEmpty(result.rows())}">
|
||||
<div class="section-heading compact-heading">
|
||||
<div>
|
||||
<h3>벡터 검색 Top-K</h3>
|
||||
<p class="section-subtitle">거리(SCORE)가 낮은 순서로 VPD를 통과한 검색 단위를 반환했습니다.</p>
|
||||
<p class="section-subtitle">거리(SCORE)가 낮은 순서로 행 접근 정책을 통과한 검색 단위를 반환했습니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-light" th:text="${'K=' + result.rowCount()}">K=0</span>
|
||||
</div>
|
||||
@@ -145,8 +162,8 @@
|
||||
<div>
|
||||
<h3>참고용 SQL 재현</h3>
|
||||
<p class="section-subtitle"
|
||||
th:text="${vectorSearch ? 'VECTOR_DISTANCE 검색과 권한 규칙을 조합한 참고용 표현입니다.' : '현재 권한 규칙을 조합한 참고용 표현입니다.'}">
|
||||
현재 권한 규칙을 조합한 참고용 표현입니다.
|
||||
th:text="${vectorSearch ? 'VECTOR_DISTANCE 검색과 행 접근 규칙을 조합한 참고용 표현입니다.' : '현재 행 접근 규칙을 조합한 참고용 표현입니다.'}">
|
||||
현재 행 접근 규칙을 조합한 참고용 표현입니다.
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge text-bg-light">참고용</span>
|
||||
@@ -154,7 +171,7 @@
|
||||
<div class="probe-exchange-grid mt-3">
|
||||
<section class="probe-exchange" data-sql-trace-field="vpd_context" th:if="${tokenContext}">
|
||||
<h3>set_vpd_context 사용자 컨텍스트</h3>
|
||||
<p class="form-hint">이 컨텍스트로 권한 규칙을 계산한 뒤 아래 VPD predicate와 effective SQL을 만들었습니다.</p>
|
||||
<p class="form-hint">이 컨텍스트로 행 접근 규칙을 계산한 뒤 아래 VPD predicate와 effective SQL을 만들었습니다.</p>
|
||||
<dl class="mb-0">
|
||||
<div><dt>CB_AGENT_CTX.USER_ID</dt><dd th:text="${tokenContext.userId()}">103</dd></div>
|
||||
<div><dt>사용자</dt><dd th:text="${tokenContext.username()}">김어드민</dd></div>
|
||||
@@ -180,7 +197,7 @@ ROWNUM <= :row_limit</pre>
|
||||
<p class="form-hint mb-0">이 부분이 검색어 벡터와 저장 벡터의 거리 계산입니다. 아래에는 실제로 등록된 역할 기반 행 접근 조건만 표시됩니다.</p>
|
||||
</section>
|
||||
<section class="probe-exchange" data-sql-trace-field="vpd_predicate">
|
||||
<h3 th:text="${vectorSearch ? '역할 기반 권한 필터 (재현)' : 'VPD 조건 (재현)'}">VPD 조건 (재현)</h3>
|
||||
<h3 th:text="${vectorSearch ? '역할 기반 권한 필터 (재현)' : '행 접근 조건 (VPD predicate 재현)'}">행 접근 조건 (VPD predicate 재현)</h3>
|
||||
<pre th:if="${result.vpdPredicate() == '1 = 1'}">1 = 1 (ALL: 추가 행 필터 없음)</pre>
|
||||
<pre th:unless="${result.vpdPredicate() == '1 = 1'}" th:text="${result.vpdPredicate()}">(DEPT_CODE = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'))</pre>
|
||||
<p class="form-hint mb-0" th:if="${vectorSearch}">선택한 사용자의 직접 역할·그룹 상속 역할에 연결된 permission rule에서 계산됩니다. 기본 whitelist 역할의 ALL은 추가 행 필터 없이 조회를 허용하고, 실제로 TAG·부서 조건을 등록한 역할만 그 조건이 SQL에 들어갑니다. 권한이 없으면 <code>1 = 0</code>입니다.</p>
|
||||
@@ -191,7 +208,7 @@ ROWNUM <= :row_limit</pre>
|
||||
</section>
|
||||
</div>
|
||||
<p class="form-hint mt-2 mb-0">
|
||||
이 영역은 권한 규칙을 읽기 쉽게 재현한 표현일 뿐, 실행 증적이 아닙니다. 실제 SQL과 VPD predicate는 위의 DB 감사 실행 증적(FGA)으로 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
이 영역은 행 접근 규칙을 읽기 쉽게 재현한 표현일 뿐, 실행 증적이 아닙니다. 실제 SQL과 VPD predicate는 위의 DB 감사 실행 증적(FGA)으로 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -209,7 +226,7 @@ ROWNUM <= :row_limit</pre>
|
||||
<p th:text="${result.nextAction()}">다음 행동</p>
|
||||
<div class="action-stack">
|
||||
<a class="btn btn-sm rw-btn-secondary" href="/effective-matrix">사용자별 최종 권한 보기</a>
|
||||
<a class="btn btn-sm rw-btn-secondary" href="/permissions">권한 규칙 보기</a>
|
||||
<a class="btn btn-sm rw-btn-secondary" href="/permissions">행 접근 규칙 보기</a>
|
||||
<a class="btn btn-sm rw-btn-secondary" href="/tokens" th:if="${result.status().name() == 'TOKEN_NOT_FOUND' || result.status().name() == 'TOKEN_INACTIVE'}">새 토큰 발급</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<div th:fragment="explanation">
|
||||
<section class="alert alert-warning mb-3" th:if="${errorMessage}" th:text="${errorMessage}">설명을 생성할 수 없습니다.</section>
|
||||
<section th:if="${explanation}" class="ai-answer">
|
||||
<div class="section-heading mb-2">
|
||||
<div>
|
||||
<h2>LLM 스크립트 설명</h2>
|
||||
<p class="section-subtitle" th:text="${explanation.script().fileName() + ' · 토큰 처리와 행 접근/ASO 적용 흐름 포함'}">script.sql</p>
|
||||
</div>
|
||||
<div class="action-stack">
|
||||
<span class="badge text-bg-secondary" th:text="${explanation.modelName()} ?: 'model not configured'">model</span>
|
||||
<span class="badge" th:classappend="${explanation.status() == 'SUCCESS'} ? ' text-bg-success' : ' text-bg-warning'" th:text="${explanation.status()}">SUCCESS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="markdown-view" data-markdown-view th:text="${explanation.answer()}">설명</div>
|
||||
<details class="explanation-details mt-3">
|
||||
<summary>LLM에 전달한 근거 Prompt 보기</summary>
|
||||
<pre th:text="${explanation.prompt()}">prompt</pre>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('VPD 권한 운영')}"></head>
|
||||
<head th:replace="~{fragments/layout :: head('데이터 접근 제어')}"></head>
|
||||
<body class="login-body">
|
||||
<main class="login-shell">
|
||||
<section class="login-panel">
|
||||
<div class="login-brand">
|
||||
<span class="login-mark">VPD</span>
|
||||
<span class="login-mark">접근</span>
|
||||
<div>
|
||||
<h1>VPD 권한 운영 콘솔</h1>
|
||||
<h1>데이터 접근 제어 콘솔</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,13 @@
|
||||
Admin Password
|
||||
<input class="form-control" name="password" type="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<label class="form-check" th:if="${rememberMeAvailable}">
|
||||
<!-- Persistent login is opt-out for this internal HTTPS backoffice.
|
||||
The server issues only a signed HttpOnly/Secure cookie; no password
|
||||
or bearer token is stored in browser Web Storage. -->
|
||||
<input class="form-check-input" id="remember-me" name="remember-me" type="checkbox" value="true" checked>
|
||||
<span class="form-check-label" for="remember-me">로그인 유지 <small class="text-muted" th:text="${'이 기기에서 ' + rememberMeDays + '일간 유지'}">이 기기에서 14일간 유지</small></span>
|
||||
</label>
|
||||
<button class="btn rw-btn-primary w-100" type="submit">로그인</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
262
src/main/resources/templates/masking-rules.html
Normal file
262
src/main/resources/templates/masking-rules.html
Normal file
@@ -0,0 +1,262 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('컬럼 마스킹')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>컬럼 마스킹</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>마스킹 방법을 사전 정의 템플릿으로 등록하고, 민감 컬럼마다 하나의 기본 규칙을 연결합니다. 컬럼 연결 목록은 <strong>마스킹 대상 블랙리스트</strong>이며, 연결하지 않은 컬럼은 ASO 마스킹 대상이 아닙니다. 행 접근은 행 접근 정책이 계속 담당하며 이 화면은 반환값 표시 방식만 다룹니다.</p>
|
||||
<p>연결된 컬럼은 행 접근 정책이 조회를 허용한 사용자에게도 기본적으로 마스킹됩니다. <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에 등록한 사용자만 원문을 봅니다. 이 설정은 행 접근 권한을 추가하지 않습니다. 컬럼 연결·해제 또는 연결된 규칙의 활성 상태 변경 시 DBMS_REDACT 정책도 자동 동기화됩니다.</p>
|
||||
<section class="content-band mt-3 mb-0">
|
||||
<h2>ASO 마스킹 적용 흐름</h2>
|
||||
<p><strong>ASO/Data Redaction은 권한 테이블을 직접 조회하지 않습니다.</strong> 토큰 처리 중 <code>set_masking_rule_context(user_id)</code>가 사용자별 마스킹 예외를 세션 context로 만들고, ASO 정책은 그 context만 읽습니다.</p>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="aso-flow-title aso-flow-desc">
|
||||
<title id="aso-flow-title">Bearer 토큰 기반 ASO 마스킹 적용 흐름</title>
|
||||
<desc id="aso-flow-desc">Bearer 토큰으로 사용자 컨텍스트와 컬럼별 MR context를 만들고, Oracle Data Redaction이 MR context 값에 따라 원문 또는 마스킹값을 반환한다.</desc>
|
||||
<defs>
|
||||
<marker id="aso-flow-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"/></marker>
|
||||
</defs>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M185 92 H225"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M385 92 H425"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M585 92 H625"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M785 92 H825"/>
|
||||
<path class="diagram-link diagram-link-secondary" marker-end="url(#aso-flow-arrow)" d="M705 212 V144"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M910 136 V205"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M810 248 H850"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M930 291 V315"/>
|
||||
<path class="diagram-link" marker-end="url(#aso-flow-arrow)" d="M850 248 H720 V315"/>
|
||||
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">요청 주체 식별</text></g>
|
||||
<g class="diagram-node"><rect x="225" y="48" width="160" height="88" rx="12"/><text x="305" y="76" class="diagram-node-title">토큰 검증</text><text x="305" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="305" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
|
||||
<g class="diagram-node"><rect x="425" y="48" width="160" height="88" rx="12"/><text x="505" y="76" class="diagram-node-title">사용자 context</text><text x="505" y="103" class="diagram-node-detail">CB_AGENT_CTX.USER_ID</text><text x="505" y="120" class="diagram-node-note">EMP_NO · DEPT_CODE</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="625" y="48" width="160" height="88" rx="12"/><text x="705" y="76" class="diagram-node-title">마스킹 context</text><text x="705" y="103" class="diagram-node-detail">set_masking_rule_</text><text x="705" y="120" class="diagram-node-detail">context(user_id)</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="825" y="48" width="245" height="88" rx="12"/><text x="947" y="76" class="diagram-node-title">컬럼별 세션 변수</text><text x="947" y="103" class="diagram-node-detail">MR_<column_id> = Y / N</text><text x="947" y="120" class="diagram-node-note">현재 DB 세션에만 설정</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="525" y="212" width="280" height="72" rx="12"/><text x="665" y="240" class="diagram-node-title">마스킹 예외·규칙 메타데이터</text><text x="665" y="263" class="diagram-node-detail">cb_user_masking_rule · cb_column_masking_rule</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="850" y="205" width="160" height="86" rx="12"/><text x="930" y="235" class="diagram-node-title">DBMS_REDACT</text><text x="930" y="260" class="diagram-node-detail">SYS_CONTEXT로 MR 읽음</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="850" y="315" width="160" height="64" rx="12"/><text x="930" y="341" class="diagram-node-title">MR = Y</text><text x="930" y="363" class="diagram-node-detail">원문 표시</text></g>
|
||||
<g class="diagram-node"><rect x="640" y="315" width="160" height="64" rx="12"/><text x="720" y="341" class="diagram-node-title">MR = N / NULL</text><text x="720" y="363" class="diagram-node-detail">마스킹 적용</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="form-hint mb-0">연결 고리: 토큰 검증 후 <code>set_masking_rule_context(v_user_id)</code>가 <code>MR_<column_id></code>를 설정하고, Data Redaction 조건식은 <code>SYS_CONTEXT('CB_AGENT_CTX', 'MR_<column_id>')</code>만 평가합니다. 자세한 SQL은 <a href="/security-sql-scripts?script=aso-masking-runtime">ASO 마스킹 런타임 스크립트</a>에서 확인하세요.</p>
|
||||
</section>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="architecture-kicker">행 접근과 분리된 컬럼 제어</span>
|
||||
<h2>ASO 마스킹 설정 순서</h2>
|
||||
<p class="section-subtitle">행 접근은 행 접근 규칙과 Oracle VPD 정책이 결정합니다. 이 화면은 허용된 행 안에서 컬럼 값을 원문으로 줄지, 마스킹해서 줄지만 관리합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="effective-preview">
|
||||
<dl>
|
||||
<div>
|
||||
<dt>대상 컬럼 등록</dt>
|
||||
<dd>마스킹 후보 블랙리스트에 DB 컬럼을 올립니다. 이 단계만으로는 값이 마스킹되지 않습니다.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>기본 규칙 연결</dt>
|
||||
<dd>컬럼에 활성 컬럼 마스킹 규칙을 연결하면 DBMS_REDACT 정책이 동기화됩니다.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>원문 표시 허용</dt>
|
||||
<dd>예외 사용자는 <a href="/user-masking-rules">컬럼 원문 표시 허용 사용자</a>에서 지정합니다.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>DB ASO 정책 동기화</h2>
|
||||
<p class="section-subtitle">이제 컬럼 기본 규칙을 연결·해제하거나 연결된 규칙을 활성·비활성화하면 DB 정책도 자동으로 반영됩니다. 아래 버튼은 자동 동기화 도입 전의 기존 설정을 한 번에 보정하거나, 운영 점검 시 현재 설정을 다시 반영할 때만 사용하세요.</p>
|
||||
<form method="post" action="/masking-rules/synchronize" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button class="btn btn-outline-primary" type="submit">현재 DB 정책 동기화</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive mt-3">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>레거시 VPD 컬럼 제어</th><th>DB 정책 활성</th><th>상태 판단</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="status : ${policyStatuses}">
|
||||
<td><code th:text="${status.targetLabel()}">POC_2.KB_CUSTOMERS</code></td>
|
||||
<td><code th:text="${status.policyName()}">KB_CUSTOMER_PII_REDACT</code></td>
|
||||
<td th:text="${status.configuredColumnCount()}">0</td>
|
||||
<td th:text="${status.appliedColumnCount()}">0</td>
|
||||
<td><span class="badge" th:classappend="${status.legacyVpdColumnPolicyCount() == 0} ? ' text-bg-secondary' : ' text-bg-danger'" th:text="${status.legacyVpdColumnPolicyCount() == 0} ? '없음' : ${status.legacyVpdColumnPolicyCount() + '건 활성'}">없음</span></td>
|
||||
<td><span class="badge" th:classappend="${status.policyEnabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${status.policyEnabled()} ? 'DB 적용 ON' : 'DB 적용 OFF'">DB 적용 OFF</span></td>
|
||||
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">미적용</span><br><small class="text-muted" th:text="${status.detail()}">설명</small></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(policyStatuses)}"><td colspan="7" class="text-muted">조회할 ASO 정책이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>사전 정의 마스킹 방식</h2>
|
||||
<p class="section-subtitle">임의 SQL이나 정규식을 직접 입력하지 않고 검증된 템플릿만 선택합니다.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>템플릿</th><th>방식</th><th>동작</th><th>ASO 구현</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="template : ${templates}">
|
||||
<td><code th:text="${template.code()}">NULLIFY</code></td>
|
||||
<td th:text="${template.label()}">값 숨김(NULL)</td>
|
||||
<td th:text="${template.description()}">설명</td>
|
||||
<td><code th:text="${template.asoFunction()}">DBMS_REDACT.NULLIFY</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>컬럼 마스킹 규칙 등록</h2>
|
||||
<p class="section-subtitle">업무용 이름을 붙여 템플릿을 재사용합니다. 예: <code>KB_RRN_STANDARD</code>.</p>
|
||||
<form method="post" action="/masking-rules" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
규칙 코드
|
||||
<input class="form-control" name="ruleCode" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_]{2,63}" required placeholder="KB_RRN_STANDARD">
|
||||
<span class="form-hint">영문·숫자·밑줄만 사용합니다.</span>
|
||||
</label>
|
||||
<label>
|
||||
규칙명
|
||||
<input class="form-control" name="ruleName" maxlength="100" required placeholder="주민번호 기본 마스킹">
|
||||
</label>
|
||||
<label>
|
||||
마스킹 템플릿
|
||||
<select class="form-select" name="templateCode" required>
|
||||
<option th:each="template : ${templates}" th:value="${template.code()}"
|
||||
th:text="${template.label()}">값 숨김(NULL)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
설명
|
||||
<input class="form-control" name="description" maxlength="400" placeholder="적용 목적 또는 업무 기준">
|
||||
</label>
|
||||
<button class="btn btn-primary" type="submit">규칙 등록</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>등록된 컬럼 마스킹 규칙</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>코드</th><th>규칙명</th><th>템플릿</th><th>설명</th><th>상태</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="rule : ${rules}">
|
||||
<td><code th:text="${rule.ruleCode()}">KB_RRN_STANDARD</code></td>
|
||||
<td th:text="${rule.ruleName()}">주민번호 기본 마스킹</td>
|
||||
<td th:text="${rule.templateLabel()}">값 숨김(NULL)</td>
|
||||
<td th:text="${rule.description() ?: '-'}">설명</td>
|
||||
<td><span class="badge" th:classappend="${rule.enabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${rule.enabledYn()}">Y</span></td>
|
||||
<td>
|
||||
<form method="post" action="/masking-rules/active" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="ruleId" th:value="${rule.ruleId()}">
|
||||
<input type="hidden" name="active" th:value="${!rule.enabled()}">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit" th:text="${rule.enabled()} ? '비활성화' : '활성화'">변경</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(rules)}"><td colspan="6" class="text-muted">등록된 컬럼 마스킹 규칙이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>마스킹 대상 컬럼 추가</h2>
|
||||
<p class="section-subtitle">DB 실제 컬럼을 ASO 마스킹 후보 블랙리스트에 등록합니다. 이 단계는 컬럼을 선택 목록에 올리는 작업이며, 실제 마스킹 적용은 아래에서 기본 규칙을 연결할 때 수행됩니다.</p>
|
||||
<form method="post" action="/masking-rules/target-columns" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
대상 객체 / 컬럼
|
||||
<select class="form-select" name="target" required>
|
||||
<option value="">선택하세요</option>
|
||||
<th:block th:each="object : ${maskingTargetObjects}">
|
||||
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(availableMaskingColumnsByObject[object.objectId()])}">
|
||||
<option th:each="columnName : ${availableMaskingColumnsByObject[object.objectId()]}"
|
||||
th:value="|${object.objectId()}:${columnName}|"
|
||||
th:text="${object.displayName() + '.' + columnName}">POC_2.KB_CONTRACTS.PREMIUM</option>
|
||||
</optgroup>
|
||||
</th:block>
|
||||
</select>
|
||||
<span class="form-hint">현재 관리 대상 ASO 정책이 있는 객체만 표시됩니다. 예: <code>POC_2.KB_CONTRACTS.PREMIUM</code>.</span>
|
||||
</label>
|
||||
<button class="btn btn-outline-primary" type="submit">대상 컬럼 추가</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>민감 컬럼에 기본 규칙 연결</h2>
|
||||
<p class="section-subtitle">이 목록이 마스킹 대상 블랙리스트입니다. 컬럼 하나에는 활성 규칙 하나만 연결할 수 있으며, 컬럼 원문 표시 허용 사용자는 별도 화면에서 지정합니다. 오른쪽 DB 상태는 컬럼이 속한 객체의 실제 Redaction 정책 상태입니다.</p>
|
||||
<form method="post" action="/masking-rules/columns" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
민감 컬럼
|
||||
<select class="form-select" name="columnId" required>
|
||||
<option value="">선택하세요</option>
|
||||
<th:block th:each="object : ${objects}">
|
||||
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(sensitiveColumnsByObject[object.objectId()])}">
|
||||
<option th:each="column : ${sensitiveColumnsByObject[object.objectId()]}"
|
||||
th:value="${column.columnId()}"
|
||||
th:text="${column.columnName() + ' [' + column.displayPolicyLabel() + ']'}">RRN_MASKED</option>
|
||||
</optgroup>
|
||||
</th:block>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
적용할 컬럼 마스킹 규칙
|
||||
<select class="form-select" name="ruleId" required>
|
||||
<option value="">선택하세요</option>
|
||||
<option th:each="rule : ${rules}" th:if="${rule.enabled()}" th:value="${rule.ruleId()}"
|
||||
th:text="${rule.ruleName() + ' · ' + rule.templateLabel()}">주민번호 기본 마스킹</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn btn-primary" type="submit">컬럼에 연결</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive mt-3">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>대상 컬럼</th><th>규칙</th><th>템플릿</th><th>백오피스 설정</th><th>DB ASO 적용 상태</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="columnRule : ${columnRules}">
|
||||
<td><code th:text="${columnRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
|
||||
<td th:text="${columnRule.ruleName()}">주민번호 기본 마스킹</td>
|
||||
<td th:text="${columnRule.template().label()}">주민등록번호 부분 마스킹</td>
|
||||
<td><span class="badge" th:classappend="${columnRule.ruleEnabled()} ? ' text-bg-success' : ' text-bg-warning'" th:text="${columnRule.ruleEnabled()} ? '기본 규칙 연결됨' : '규칙 비활성'">기본 규칙 연결됨</span></td>
|
||||
<td th:with="policyStatus=${policyStatusByObjectName[columnRule.objectName()]}">
|
||||
<span class="badge"
|
||||
th:if="${policyStatus}"
|
||||
th:classappend="${' ' + policyStatus.badgeClass()}"
|
||||
th:text="${policyStatus.statusLabel()}">적용됨</span>
|
||||
<span class="badge text-bg-secondary" th:unless="${policyStatus}">관리 정책 없음</span>
|
||||
<div class="text-muted small" th:if="${policyStatus}" th:text="${policyStatus.detail()}">상세</div>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/masking-rules/columns/delete" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="columnId" th:value="${columnRule.columnId()}">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">연결 해제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(columnRules)}"><td colspan="6" class="text-muted">민감 컬럼에 연결된 컬럼 마스킹 규칙이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1 class="h3">대화형 검색</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>질문을 MCP tool로 라우팅하고, 권한 태그·VPD 행 제한·ORDS 결과를 답변으로 정리합니다.</p>
|
||||
<p>질문을 MCP tool로 라우팅하고, 권한 태그·행 접근 제한·ORDS 결과를 답변으로 정리합니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<p>추천 질문을 선택하거나 질문을 직접 입력하세요.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>세션과 조회 결과 설명 보기</summary>
|
||||
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 검증 세션으로 실제 ORDS/VPD 결과를 조회할 수 있습니다.</p>
|
||||
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 검증 세션으로 실제 ORDS 행 접근 결과를 조회할 수 있습니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<label>
|
||||
질문
|
||||
<textarea class="form-control" id="mcp-chat-question" name="question" rows="3"
|
||||
placeholder="예: BOARD_POSTS에서 이 토큰으로 보이는 행과 NULL 처리 컬럼을 요약해줘." required></textarea>
|
||||
placeholder="예: BOARD_POSTS에서 이 토큰으로 보이는 행과 ASO 마스킹 컬럼을 요약해줘." required></textarea>
|
||||
</label>
|
||||
<div class="question-presets" aria-label="질문 예시">
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
@@ -64,15 +64,15 @@
|
||||
문서 조회 요약
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
data-question="CB_V_SEARCH_DOCUMENTS에서 VPD가 제외한 행과 contents 표시 보호 여부를 구분하고, 보이는 dept_code와 owner_emp_no를 정리해줘.">
|
||||
data-question="CB_V_SEARCH_DOCUMENTS에서 행 접근 정책이 제외한 행과 contents 표시 보호 여부를 구분하고, 보이는 dept_code와 owner_emp_no를 정리해줘.">
|
||||
민감 컬럼 확인
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 SPRING_BOOT 또는 ORACLE_VPD 태그 권한으로 검색 가능한 검색 단위와 VPD 결과를 요약해줘.">
|
||||
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 SPRING_BOOT 또는 ORACLE_VPD 태그 권한으로 검색 가능한 검색 단위와 행 접근 결과를 요약해줘.">
|
||||
태그 벡터 검색
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
data-question="질문과 가장 가까운 ORDS/VPD MCP tool을 선택하고, 선택 근거와 필요한 bearer token 흐름만 보여줘.">
|
||||
data-question="질문과 가장 가까운 ORDS 행 접근 MCP tool을 선택하고, 선택 근거와 필요한 bearer token 흐름만 보여줘.">
|
||||
라우팅만 확인
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>검색 해석</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>질문을 근거로 MCP tool을 고르고, Bearer Token으로 VPD/ORDS 결과를 조회한 뒤 모델이 설명합니다.</p>
|
||||
<p>질문을 근거로 MCP tool을 고르고, Bearer Token으로 ORDS 행 접근 결과를 조회한 뒤 모델이 설명합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
<label class="span-2">
|
||||
질문
|
||||
<textarea class="form-control" id="mcp-reasoning-question" name="question" rows="3"
|
||||
placeholder="비워도 조회 행, NULL 처리, 권한 범위, 다음 조치를 요약합니다."></textarea>
|
||||
placeholder="비워도 조회 행, ASO 마스킹 컬럼, 권한 범위, 다음 조치를 요약합니다."></textarea>
|
||||
</label>
|
||||
<div class="question-presets span-2" aria-label="질문 예시">
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 이 토큰이 볼 수 있는 기술 태그 검색 단위 수와 VPD가 제외한 범위를 요약해줘.">
|
||||
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 이 토큰이 볼 수 있는 기술 태그 검색 단위 수와 행 접근 정책이 제외한 범위를 요약해줘.">
|
||||
기본 분석
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
@@ -71,7 +71,7 @@
|
||||
태그 권한 범위 판단
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
data-question="ORDS path, VPD policy, TAG permission, token 상태 중 이상 징후를 먼저 bullet로 요약하고 다음 확인 순서를 제시해줘.">
|
||||
data-question="ORDS path, 행 접근 policy, TAG permission, token 상태 중 이상 징후를 먼저 bullet로 요약하고 다음 확인 순서를 제시해줘.">
|
||||
운영 점검 요약
|
||||
</button>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@
|
||||
<tr th:each="tool : ${tools}">
|
||||
<td><code th:text="${tool.name()}">ords.query.admin.board_posts</code></td>
|
||||
<td><code th:text="${tool.ordsPath()}">path</code></td>
|
||||
<td th:text="${tool.description()}">VPD/ORDS 조회 도구</td>
|
||||
<td th:text="${tool.description()}">ORDS 행 접근 조회 도구</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(tools)}">
|
||||
<td colspan="3" class="text-muted">등록된 보호 객체 도구가 없습니다.</td>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>MCP 연동</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>보호 객체 ORDS 조회를 외부 MCP client에서 호출할 수 있도록 context path별 SSE transport로 제공합니다.</p>
|
||||
<p>사용자 Bearer Token을 전달해 GPT-5.4-mini Select AI 자연어 조회를 실행하는 단일 MCP tool을 제공합니다. Select AI는 comment, annotation, constraint 메타데이터를 함께 사용하고, DB에서는 토큰 기준 행 접근 context가 적용됩니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Auth</th>
|
||||
<td><code>Basic Auth</code> / <code>BACKOFFICE_ADMIN_USER</code>, <code>BACKOFFICE_ADMIN_PASSWORD</code></td>
|
||||
<td><code>Authorization: Bearer <사용자 Bearer Token></code> — 이 토큰 하나로 행 접근 컨텍스트를 설정합니다.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Methods</th>
|
||||
@@ -97,12 +97,9 @@
|
||||
<td th:text="${tool.displayName()}">ADMIN.BOARD_POSTS</td>
|
||||
<td><code th:text="${tool.ordsPath()}">cb-ords/cb-object-query/admin/board_posts</code></td>
|
||||
<td>
|
||||
<div th:text="${tool.description()}">VPD/ORDS 조회 도구 설명</div>
|
||||
<div th:text="${tool.description()}">ORDS 행 접근 조회 도구 설명</div>
|
||||
<small class="text-muted">
|
||||
<code>bearerToken</code> → Authorization · <code>limit</code> → ORDS limit · object → 이 도구에 고정
|
||||
<span th:if="${tool.displayName().toUpperCase().endsWith('CB_VECTOR_SEARCH_DOCUMENTS')}">
|
||||
· <code>embedding[]</code> → 검색 JSON 본문
|
||||
</span>
|
||||
HTTP <code>Authorization</code> → 행 접근 컨텍스트 · <code>prompt</code> → GPT-5.4-mini Select AI 자연어 질의 · <code>limit</code> → 최대 반환 행 수
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,10 +116,10 @@
|
||||
<summary>tools/call parameter 예시 보기</summary>
|
||||
<h2>tools/call Arguments</h2>
|
||||
<pre class="code-block">{
|
||||
"bearerToken": "vpd_live_xxx",
|
||||
"prompt": "KB_CLAIMS의 전체 청구 건수를 조회해 줘.",
|
||||
"limit": 50
|
||||
}</pre>
|
||||
<p class="form-hint">벡터 검색 tool인 <code>CB_VECTOR_SEARCH_DOCUMENTS</code>는 여기에 외부 임베딩 모델이 만든 <code>embedding</code> 숫자 배열을 추가합니다. 토큰·limit은 모든 tool에 공통이고, embedding은 벡터 tool에만 필요합니다.</p>
|
||||
<p class="form-hint">등록 tool은 <code>ords.query.kb_select_ai_vpd</code> 하나입니다. HTTP Authorization의 사용자 Bearer Token으로 컨텍스트를 설정한 뒤 GPT-5.4-mini가 comment, annotation, constraint를 참고해 생성한 검증된 읽기 전용 KB 원장 SQL만 실행합니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -156,9 +153,9 @@
|
||||
"id": 3,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "ords.query.admin.board_posts",
|
||||
"name": "ords.query.kb_select_ai_vpd",
|
||||
"arguments": {
|
||||
"bearerToken": "vpd_live_xxx",
|
||||
"prompt": "KB_CLAIMS의 전체 청구 건수를 조회해 줘.",
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>조회 대상</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>이 화면은 DB 객체를 HTTP로 연결할 경로만 등록합니다. 누가 어떤 행을 볼 수 있는지는 권한 규칙과 VPD에서 결정합니다.</p>
|
||||
<p>이 화면은 DB 객체를 HTTP로 연결할 경로만 등록합니다. 누가 어떤 행을 볼 수 있는지는 행 접근 규칙과 Oracle VPD 정책에서 결정합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<strong>지식 검색 연결:</strong> 문서 검색 단위와 기술 태그를 등록한 뒤 전용 ORDS 검색을 연결합니다.
|
||||
<details class="explanation-details mt-2">
|
||||
<summary>지식 검색 연결 흐름 보기</summary>
|
||||
<p>문서를 검색 단위로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 검색 단위만 검색됩니다.</p>
|
||||
<p>문서를 검색 단위로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">행 접근 규칙</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 검색 단위만 검색됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 기본 구성입니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>대상 추가 후 생성되는 내용 보기</summary>
|
||||
<p>대상 추가 후 Handler를 생성하면 VPD context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
|
||||
<p>대상 추가 후 Handler를 생성하면 행 접근 context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
|
||||
</details>
|
||||
<form method="post" action="/objects" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
@@ -66,7 +66,7 @@
|
||||
</form>
|
||||
<details class="explanation-details explanation-warning mt-3">
|
||||
<summary>기본 Handler 구성과 수정 가이드 보기</summary>
|
||||
<p><code>cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id)</code>로 토큰의 사용자·역할 컨텍스트와 검증 요청 식별자를 넣고, 선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다. 이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다. 컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/permissions">권한 관리의 원문 표시 허용 컬럼</a>에서 별도로 설정합니다.</p>
|
||||
<p><code>cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id)</code>로 토큰의 사용자·역할 컨텍스트와 검증 요청 식별자를 넣고, 선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다. 이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다. 컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/masking-rules">컬럼 마스킹</a>에서 별도로 설정합니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
<details class="explanation-details">
|
||||
<summary>이 대상의 자동 연결 흐름 보기</summary>
|
||||
<div class="object-handler-explainer">
|
||||
<strong>토큰 헤더 → VPD context 설정 → 선택한 단일 테이블의 기본 SELECT → JSON 응답</strong>
|
||||
<small>행 접근은 권한 규칙/VPD가 담당합니다. 원문 표시 예외는 권한 관리 Step 4에서 여러 컬럼을 등록하세요.</small>
|
||||
<strong>토큰 헤더 → 행 접근 context 설정 → 선택한 단일 테이블의 기본 SELECT → JSON 응답</strong>
|
||||
<small>행 접근은 행 접근 규칙과 Oracle VPD 정책이 담당합니다. 컬럼 원문/마스킹은 컬럼 마스킹의 ASO/Data Redaction 설정에서 관리하세요.</small>
|
||||
</div>
|
||||
</details>
|
||||
<div th:if="${object.objectName() != 'CB_VECTOR_SEARCH_DOCUMENTS'}"
|
||||
|
||||
@@ -8,11 +8,39 @@
|
||||
<h1>운영 현황</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>보호 객체별 ORDS handler, VPD policy, 권한 rule, 최근 검증 상태를 확인합니다. 이상이 있을 때는 해당 객체의 권한·VPD·ORDS 순서로 점검하세요.</p>
|
||||
<p>보호 객체별 ORDS handler, 행 접근 정책(VPD), 행 접근 rule, 최근 검증 상태를 확인합니다. 이상이 있을 때는 해당 객체의 행 접근 규칙·VPD 정책·ORDS 순서로 점검하세요.</p>
|
||||
<p>컬럼 마스킹은 ASO/Data Redaction 정책 상태로 별도 확인합니다. 행 접근 정책이 정상이더라도 ASO 정책이 불일치하면 민감 컬럼이 예상과 다르게 원문 또는 마스킹값으로 반환될 수 있습니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="architecture-kicker">운영 판단 기준</span>
|
||||
<h2>행 접근 정책(VPD)과 컬럼 마스킹 정책(ASO)을 분리해서 봅니다</h2>
|
||||
<p class="section-subtitle">VPD는 행 접근, ASO/Data Redaction은 컬럼 원문/마스킹을 담당합니다. 둘 중 하나만 정상이어도 전체 권한 결과가 정상이라고 판단하면 안 됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="effective-preview">
|
||||
<dl>
|
||||
<div>
|
||||
<dt>행 접근 정책(VPD)</dt>
|
||||
<dd>아래 보호 객체 표에서 policy, function, 최근 접근 검증 상태를 확인합니다.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>ASO 컬럼 정책</dt>
|
||||
<dd>아래 ASO 표에서 백오피스 활성 컬럼 수와 DB Redaction 컬럼 수가 일치하는지 확인합니다.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>실제 결과 검증</dt>
|
||||
<dd>상태가 정상이어도 최종 판단은 <a href="/probe">접근 검증</a>에서 토큰 기준 응답으로 확인합니다.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>행 접근 정책(VPD) · ORDS 검증 상태</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle operation-status-table">
|
||||
<thead>
|
||||
@@ -75,6 +103,34 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>ASO 컬럼 마스킹 정책 상태</h2>
|
||||
<p class="section-subtitle">컬럼 마스킹 화면의 백오피스 설정과 실제 Oracle Data Redaction 정책을 비교한 결과입니다.</p>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-secondary" href="/masking-rules">컬럼 마스킹 열기</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>상태</th><th>확인 결과</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="status : ${maskingPolicyStatuses}">
|
||||
<td><code th:text="${status.targetLabel()}">POC_2.KB_CLAIMS</code></td>
|
||||
<td><code th:text="${status.policyName()}">KB_CLAIM_AMOUNT_REDACT</code></td>
|
||||
<td th:text="${status.configuredColumnCount()}">0</td>
|
||||
<td th:text="${status.appliedColumnCount()}">0</td>
|
||||
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">적용됨</span></td>
|
||||
<td th:text="${status.detail()}">활성 규칙과 DB 정책 컬럼이 일치합니다.</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(maskingPolicyStatuses)}">
|
||||
<td colspan="6" class="text-muted">조회할 ASO 정책 상태가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<h1>조회 연동</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 SQL_ID 증적과 권한 조건 재현은 접근 검증 메뉴에서 확인할 수 있습니다.</p>
|
||||
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 행 접근 정책(VPD)이 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 SQL_ID 증적과 행 조건 재현은 접근 검증 메뉴에서 확인할 수 있습니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
권한별로 VPD가 붙인 실제 행 조건을 확인하려면 <a href="/probe">접근 검증</a>에서 이 Handler의 보호 객체를 실행하세요. SQL trace를 반환하도록 갱신된 Handler는 같은 ORDS 세션의 <code>CB_AGENT_DOC_VPD_FILTER</code> predicate를 함께 표시합니다.
|
||||
사용자별로 적용된 실제 행 조건을 확인하려면 <a href="/probe">접근 검증</a>에서 이 Handler의 보호 객체를 실행하세요. SQL trace를 반환하도록 갱신된 Handler는 같은 ORDS 세션의 <code>CB_AGENT_DOC_VPD_FILTER</code> predicate를 함께 표시합니다.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('접근 규칙')}"></head>
|
||||
<head th:replace="~{fragments/layout :: head('행 접근 규칙')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>접근 규칙</h1>
|
||||
<h1>행 접근 규칙</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>VPD policy function이 참조할 사용자, 역할, 행 규칙, 컬럼 원문 허용 규칙을 저장합니다. KB 조건 코드는 토큰 context 값으로 치환되고, 정적 SQL 조건은 대상 객체의 WHERE 절에 그대로 추가됩니다. 한 권한의 규칙은 AND, 서로 다른 ALLOW 권한은 OR로 합치며 DENY는 허용 결과에서 제외합니다.</p>
|
||||
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. 저장값은 최종 SQL이 아니라 <code>CB_AGENT_DOC_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꾸는 매핑 데이터입니다.</p>
|
||||
<p>컬럼 원문/마스킹은 이 화면에서 처리하지 않습니다. 주민번호, 보험료, 청구금액 같은 컬럼 표시는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 관리합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -18,15 +19,14 @@
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>새 접근 규칙</h2>
|
||||
<p class="section-description">저장 전 적용 대상과 행 범위를 검토한 뒤 VPD 권한을 반영합니다.</p>
|
||||
<h2>새 행 접근 규칙</h2>
|
||||
<p class="section-description">저장 전 적용 대상과 행 범위를 검토한 뒤 행 접근 규칙을 반영합니다.</p>
|
||||
</div>
|
||||
<div class="wizard-progress" aria-label="권한 추가 단계">
|
||||
<button class="wizard-step-indicator active" type="button" data-wizard-target="1">역할</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="2">대상</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="3">접근 범위</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="4">표시 예외</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="5">검토</button>
|
||||
<button class="wizard-step-indicator" type="button" data-wizard-target="4">검토</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/permissions" class="permission-wizard" data-permission-wizard>
|
||||
@@ -138,7 +138,7 @@
|
||||
<div class="wizard-panel-heading">
|
||||
<div>
|
||||
<h3>권한 효과와 행 규칙</h3>
|
||||
<p>허용/거부 방향과 VPD 행 필터 조건을 설정합니다.</p>
|
||||
<p>허용/거부 방향과 행 필터 조건을 설정합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
@@ -179,40 +179,17 @@
|
||||
</div>
|
||||
<details class="explanation-details">
|
||||
<summary>행 규칙의 두 가지 적용 방식 보기</summary>
|
||||
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong>은 <code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 VPD WHERE 절에 직접 들어가지 않습니다.</p>
|
||||
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong>은 <code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 최종 WHERE 절에 직접 들어가지 않습니다.</p>
|
||||
<p class="wizard-hint"><strong>컬럼 원문/마스킹은 제외했습니다.</strong> 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="4">
|
||||
<div class="wizard-panel-heading">
|
||||
<div>
|
||||
<h3>권한별 원문 표시 예외</h3>
|
||||
<p>이 역할이 이미 볼 수 있는 행에서 마스킹을 제외할 컬럼을 여러 개 선택합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="masking-column-picker">
|
||||
<div class="field-block-title">선택 가능한 표시 보호 컬럼</div>
|
||||
<div class="question-presets" data-maskable-column-list>
|
||||
<span class="text-muted small">보호 객체를 선택하면 마스킹 대상 컬럼이 표시됩니다.</span>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
원문 표시 허용(마스킹 제외) 컬럼 · 여러 개 가능
|
||||
<input class="form-control" name="visibleColumns" placeholder="예: CONTENTS, SOURCE_URI">
|
||||
<span class="selected-column-list" data-selected-visible-columns aria-live="polite"></span>
|
||||
</label>
|
||||
<details class="explanation-details">
|
||||
<summary>원문 표시 허용 컬럼 안내 보기</summary>
|
||||
<p class="wizard-hint">쉼표로 여러 컬럼을 등록하거나 위 버튼을 여러 번 누르세요. 등록하지 않은 민감 컬럼은 기본 표시 보호를 유지합니다. 행을 볼 수 있는지는 이 목록이 아니라 권한 규칙과 VPD가 결정합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="wizard-panel" data-wizard-step="5">
|
||||
<div class="wizard-panel-heading">
|
||||
<div>
|
||||
<h3>저장 전 검토</h3>
|
||||
<p>저장될 권한과 예상 effective policy를 확인합니다.</p>
|
||||
<p>저장될 행 접근 규칙과 VPD predicate 변환 예시를 확인합니다. 컬럼 마스킹은 ASO 화면에서 별도로 설정합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="policy-preview" data-policy-preview>
|
||||
@@ -222,10 +199,9 @@
|
||||
<div><dt>보호 객체</dt><dd data-preview="object">-</dd></div>
|
||||
<div><dt>적용 대상</dt><dd data-preview="affectedPrincipals">-</dd></div>
|
||||
<div><dt>객체 컬럼</dt><dd data-preview="objectColumnsFinal">-</dd></div>
|
||||
<div><dt>행 접근</dt><dd data-preview="rowPolicy">-</dd></div>
|
||||
<div><dt>행 접근 업무 의미</dt><dd data-preview="rowPolicy">-</dd></div>
|
||||
<div><dt>VPD predicate 예상</dt><dd data-preview="predicatePreview">-</dd></div>
|
||||
<div><dt>권한별 컬럼 마스킹</dt><dd data-preview="columnPolicy">-</dd></div>
|
||||
<div><dt>NULL 처리 예상</dt><dd data-preview="nullPolicy">-</dd></div>
|
||||
<div><dt>컬럼 마스킹</dt><dd data-preview="columnPolicy">-</dd></div>
|
||||
<div class="policy-preview-emphasis"><dt>저장 영향</dt><dd data-preview="saveGuard">-</dd></div>
|
||||
<div><dt>되돌리기</dt><dd>저장 후 아래 권한 목록에서 삭제할 수 있습니다. 이 객체의 마지막 권한을 삭제하면 보호 객체가 비활성화될 수 있습니다.</dd></div>
|
||||
</dl>
|
||||
@@ -241,8 +217,8 @@
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>접근 규칙 목록</h2>
|
||||
<p class="section-subtitle">한 접근 규칙 안에서는 조건 코드와 정적 SQL 조건을 AND로 합쳐 범위를 좁힙니다. 같은 보호 대상의 서로 다른 ALLOW 권한은 OR, DENY 권한은 허용 결과에서 제외됩니다.</p>
|
||||
<h2>행 접근 규칙 목록</h2>
|
||||
<p class="section-subtitle">업무 의미를 먼저 보고, 세부 저장값과 VPD predicate 변환 예시는 행의 상세에서 확인하세요. 컬럼 원문/마스킹은 컬럼 마스킹에서 관리합니다.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle permission-list-table">
|
||||
<thead>
|
||||
@@ -250,8 +226,8 @@
|
||||
<th>역할</th>
|
||||
<th>보호 대상</th>
|
||||
<th>효과</th>
|
||||
<th>접근 범위</th>
|
||||
<th>표시 예외</th>
|
||||
<th>업무 접근 범위</th>
|
||||
<th>컬럼 마스킹</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -265,11 +241,20 @@
|
||||
th:classappend="${permission.permissionEffect() == 'DENY'} ? ' text-bg-danger' : ' text-bg-success'"
|
||||
th:text="${permission.permissionEffect()}">ALLOW</span>
|
||||
</td>
|
||||
<td><pre class="table-pre" th:text="${permission.filterPreview() ?: permission.rules() ?: '-'}">ALL ROWS</pre></td>
|
||||
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
|
||||
<td><pre class="table-pre" th:text="${permission.businessRuleSummary()}">전체 행</pre></td>
|
||||
<td>
|
||||
<span class="badge text-bg-light">ASO 화면에서 관리</span>
|
||||
<div class="text-muted small" th:text="${permission.columnControlSummary()}">컬럼 원문/마스킹은 컬럼 마스킹에서 관리</div>
|
||||
</td>
|
||||
<td>
|
||||
<details class="row-management">
|
||||
<summary>관리</summary>
|
||||
<div class="delete-impact">
|
||||
<span>저장 규칙</span>
|
||||
<pre class="table-pre mt-1" th:text="${permission.storedRuleSummary()}">CUST_ID OWN_CUSTOMER</pre>
|
||||
<span>VPD predicate 변환 예시</span>
|
||||
<pre class="table-pre mt-1" th:text="${permission.vpdMappingSummary()}">EXISTS (...)</pre>
|
||||
</div>
|
||||
<div class="delete-impact">
|
||||
<span>이 역할은 이 대상의 SELECT 권한을 잃습니다.</span>
|
||||
<strong th:text="${permission.roleName() + ' → ' + permission.objectName()}">ROLE → OBJECT</strong>
|
||||
@@ -289,7 +274,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(permissions)}">
|
||||
<td colspan="6" class="text-muted">등록된 접근 규칙이 없습니다.</td>
|
||||
<td colspan="6" class="text-muted">등록된 행 접근 규칙이 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<p>토큰과 대상 객체를 입력하면 실제 조회 결과를 보여줍니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>권한 판정 과정 보기</summary>
|
||||
<p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD가 저장된 행·열 규칙을 적용한 결과를 보여줍니다.</p>
|
||||
<p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD는 저장된 행 규칙을 적용합니다. 컬럼 원문/마스킹은 ASO/Data Redaction 결과로 별도 확인합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
<a class="btn rw-btn-secondary" href="/tokens">검증 세션이 없나요? 먼저 발급하기</a>
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
<h1>역할</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p th:if="${backofficeTrack == 'DDS'}">역할은 사용자와 접근 태그 규칙을 연결하는 기준입니다. 실제 접근 범위는 접근 규칙에서 관리합니다.</p>
|
||||
<p th:unless="${backofficeTrack == 'DDS'}">사용자에게 부여할 역할을 관리합니다. 표시 보호 등급 상한은 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고값이며, 행 접근 권한은 권한 규칙과 VPD에서 결정합니다.</p>
|
||||
<p>사용자에게 부여할 역할을 관리합니다. 표시 보호 등급 상한은 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고값이며, 행 접근 권한은 행 접근 규칙과 Oracle VPD 정책에서 결정합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +19,7 @@
|
||||
<h2>역할 추가</h2>
|
||||
<form method="post" action="/roles" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input th:if="${backofficeTrack == 'DDS'}" type="hidden" name="maxSensitivityLevel" value="PUBLIC">
|
||||
<label th:unless="${backofficeTrack == 'DDS'}">
|
||||
<label>
|
||||
역할명
|
||||
<input class="form-control" name="roleName" placeholder="HR_DEPT_ROLE" required>
|
||||
</label>
|
||||
@@ -37,7 +35,7 @@
|
||||
<option value="CONFIDENTIAL">CONFIDENTIAL</option>
|
||||
<option value="RESTRICTED">RESTRICTED</option>
|
||||
</select>
|
||||
<span class="form-hint">이 역할이 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고 상한입니다. 행을 볼 수 있는지는 권한 규칙과 VPD가 결정하며, 이 값은 행 권한을 부여하지 않습니다.</span>
|
||||
<span class="form-hint">이 역할이 허용된 행에서 원문으로 볼 수 있는 컬럼 등급의 참고 상한입니다. 행을 볼 수 있는지는 행 접근 규칙과 Oracle VPD 정책이 결정하며, 이 값은 행 권한을 부여하지 않습니다.</span>
|
||||
</label>
|
||||
<button class="btn rw-btn-primary" type="submit">추가</button>
|
||||
</form>
|
||||
@@ -51,7 +49,7 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>역할명</th>
|
||||
<th th:unless="${backofficeTrack == 'DDS'}">표시 보호 등급 상한</th>
|
||||
<th>표시 보호 등급 상한</th>
|
||||
<th>삭제 영향</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -62,7 +60,7 @@
|
||||
hasDependencies=${impact != null && (!#lists.isEmpty(impact.directUsers()) || !#lists.isEmpty(impact.groups()) || impact.permissionCount() > 0)}">
|
||||
<td th:text="${role.roleId()}">10</td>
|
||||
<td th:text="${role.roleName()}">HR_DEPT_ROLE</td>
|
||||
<td th:unless="${backofficeTrack == 'DDS'}">
|
||||
<td>
|
||||
<span class="badge text-bg-light" th:title="${'허용된 행의 컬럼 표시 상한: ' + role.maxSensitivityLevel()}"
|
||||
th:text="${role.maxSensitivityLevel()}">PUBLIC</span>
|
||||
<form method="post" action="/roles/max-sensitivity" class="inline-form">
|
||||
@@ -113,7 +111,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(roles)}">
|
||||
<td th:colspan="${backofficeTrack == 'DDS'} ? 4 : 5" class="text-muted">등록된 역할이 없습니다.</td>
|
||||
<td colspan="5" class="text-muted">등록된 역할이 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
198
src/main/resources/templates/schema-metadata.html
Normal file
198
src/main/resources/templates/schema-metadata.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('DB 메타데이터')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<section class="page-title">
|
||||
<span class="badge text-bg-primary">Select AI 근거 메타데이터</span>
|
||||
<h1 class="mt-2">DB 메타데이터</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>
|
||||
<code>POC_2</code> KB 업무 테이블의 table/column comment와 Oracle annotation을 조회·수정합니다.
|
||||
Select AI profile의 <code>comments=true</code>, <code>annotations=true</code> 설정에서는 이 값들이 SQL 생성 근거로 들어갑니다.
|
||||
</p>
|
||||
<p class="mb-0">임의 스키마나 임의 테이블은 수정하지 않고, 백오피스가 승인한 7개 업무 테이블만 대상으로 합니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="alert alert-success" th:if="${message}" th:text="${message}">저장 완료</section>
|
||||
<section class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">오류</section>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>테이블 선택</h2>
|
||||
<p class="section-subtitle">정형 MCP/Select AI가 참조하는 KB 업무 원장 7개만 표시합니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary" th:text="${#lists.size(tables)}">7</span>
|
||||
</div>
|
||||
<div class="structured-table-grid">
|
||||
<a th:each="entry : ${tables}"
|
||||
class="structured-table-card"
|
||||
th:classappend="${entry.key() == selectedKey} ? ' is-selected'"
|
||||
th:href="@{/schema-metadata(table=${entry.key()})}">
|
||||
<strong th:text="${entry.businessName()}">고객원장</strong>
|
||||
<code th:text="${entry.tableName()}">KB_CUSTOMERS</code>
|
||||
<small th:text="${entry.description()}">고객 기본정보</small>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band" th:if="${metadata}">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="badge text-bg-secondary">TABLE</span>
|
||||
<h2 class="mt-2" th:text="${metadata.table().businessName()}">계약원장</h2>
|
||||
<p class="section-subtitle">
|
||||
<code th:text="${'POC_2.' + metadata.table().tableName()}">POC_2.KB_CONTRACTS</code>
|
||||
<span th:text="${' · ' + metadata.table().description()}"> · 설명</span>
|
||||
</p>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-secondary" th:href="@{/structured-data(table=${metadata.table().key()})}">데이터 미리보기</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/schema-metadata/table-comment" class="mb-4">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="table" th:value="${metadata.table().key()}">
|
||||
<label class="form-label fw-semibold">Table comment</label>
|
||||
<textarea class="form-control" rows="3" name="comment" maxlength="4000"
|
||||
th:text="${metadata.tableComment()}"></textarea>
|
||||
<div class="form-hint mt-1">Oracle <code>COMMENT ON TABLE</code>로 즉시 반영됩니다. 빈 값으로 저장하면 comment를 비웁니다.</div>
|
||||
<button class="btn rw-btn-primary mt-2" type="submit">테이블 comment 저장</button>
|
||||
</form>
|
||||
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3>Table annotations</h3>
|
||||
<p class="section-subtitle">기존 annotation은 값 수정 또는 빈 값 저장으로 삭제할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th style="width: 220px;">Annotation</th><th>Value</th><th style="width: 110px;"></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="annotation : ${metadata.tableAnnotations()}">
|
||||
<td><code th:text="${annotation.name()}">DISPLAY_NAME</code></td>
|
||||
<td>
|
||||
<form method="post" action="/schema-metadata/table-annotation" class="metadata-inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="table" th:value="${metadata.table().key()}">
|
||||
<input type="hidden" name="annotationName" th:value="${annotation.name()}">
|
||||
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
|
||||
th:text="${annotation.value()}"></textarea>
|
||||
<div class="form-hint mt-1">저장 전 기존 같은 key는 DROP 후 다시 ADD합니다.</div>
|
||||
<button class="btn btn-sm rw-btn-primary mt-2" type="submit">저장</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-muted">기존</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(metadata.tableAnnotations())}">
|
||||
<td colspan="3" class="text-muted">등록된 table annotation이 없습니다.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<form method="post" action="/schema-metadata/table-annotation" class="row g-2 align-items-start">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="table" th:value="${metadata.table().key()}">
|
||||
<div class="col-md-3">
|
||||
<input class="form-control" name="annotationName" placeholder="예: QUERY_HINT" maxlength="128">
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
|
||||
placeholder="Select AI가 참고할 업무 의미, 조인 힌트, 값 도메인 등을 입력"></textarea>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn rw-btn-secondary w-100" type="submit">추가</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band" th:if="${metadata}">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Column comments / annotations</h2>
|
||||
<p class="section-subtitle">컬럼 comment와 컬럼 annotation을 수정합니다. 저장 즉시 DB dictionary에 반영됩니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary" th:text="${#lists.size(metadata.columns()) + '개 컬럼'}">0개 컬럼</span>
|
||||
</div>
|
||||
|
||||
<div class="metadata-column-stack">
|
||||
<details class="content-band nested-content-band"
|
||||
th:each="column : ${metadata.columns()}"
|
||||
th:open="${!#lists.isEmpty(column.annotations())}">
|
||||
<summary class="d-flex justify-content-between align-items-center gap-3">
|
||||
<span>
|
||||
<code th:text="${column.columnName()}">CONTRACT_NO</code>
|
||||
<small class="text-muted ms-2" th:text="${column.dataType()}">VARCHAR2(30)</small>
|
||||
<span class="badge text-bg-light ms-2" th:text="${column.nullable()} ? 'NULL 허용' : 'NOT NULL'">NOT NULL</span>
|
||||
</span>
|
||||
<span class="text-muted" th:text="${#lists.size(column.annotations()) + ' annotations'}">0 annotations</span>
|
||||
</summary>
|
||||
|
||||
<form method="post" action="/schema-metadata/column-comment" class="mt-3">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="table" th:value="${metadata.table().key()}">
|
||||
<input type="hidden" name="column" th:value="${column.columnName()}">
|
||||
<label class="form-label fw-semibold">Column comment</label>
|
||||
<textarea class="form-control" rows="2" name="comment" maxlength="4000"
|
||||
th:text="${column.comment()}"></textarea>
|
||||
<button class="btn btn-sm rw-btn-primary mt-2" type="submit">컬럼 comment 저장</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive mt-3">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th style="width: 220px;">Annotation</th><th>Value</th><th style="width: 110px;"></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="annotation : ${column.annotations()}">
|
||||
<td><code th:text="${annotation.name()}">DISPLAY_NAME</code></td>
|
||||
<td>
|
||||
<form method="post" action="/schema-metadata/column-annotation">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="table" th:value="${metadata.table().key()}">
|
||||
<input type="hidden" name="column" th:value="${column.columnName()}">
|
||||
<input type="hidden" name="annotationName" th:value="${annotation.name()}">
|
||||
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
|
||||
th:text="${annotation.value()}"></textarea>
|
||||
<button class="btn btn-sm rw-btn-primary mt-2" type="submit">저장</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-muted">기존</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(column.annotations())}">
|
||||
<td colspan="3" class="text-muted">등록된 column annotation이 없습니다.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<form method="post" action="/schema-metadata/column-annotation" class="row g-2 align-items-start">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="table" th:value="${metadata.table().key()}">
|
||||
<input type="hidden" name="column" th:value="${column.columnName()}">
|
||||
<div class="col-md-3">
|
||||
<input class="form-control" name="annotationName" placeholder="예: VALUE_DOMAIN" maxlength="128">
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<textarea class="form-control" rows="2" name="annotationValue" maxlength="4000"
|
||||
placeholder="컬럼 표시명, 값 범위, 조인 대상, 민감도 등을 입력"></textarea>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn rw-btn-secondary w-100" type="submit">추가</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
66
src/main/resources/templates/security-sql-scripts.html
Normal file
66
src/main/resources/templates/security-sql-scripts.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('보안 SQL 스크립트')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<section class="page-title">
|
||||
<span class="badge text-bg-primary">형상 기준</span>
|
||||
<h1 class="mt-2">보안 SQL 스크립트</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>ASO 마스킹, ORDS, Select AI 행 접근에 사용하는 Git 형상 기준 SQL을 읽기 전용으로 표시합니다. 이 화면은 SQL을 실행하거나 DB의 현재 source를 바꾸지 않습니다.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">조회할 수 없습니다.</section>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>등록된 스크립트</h2>
|
||||
<p class="section-subtitle">배포 JAR에는 아래 Git 추적 파일만 포함됩니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary" th:text="${#lists.size(scripts)}">0</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>구분</th><th>파일</th><th>용도</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="item : ${scripts}" th:classappend="${selectedScript != null and item.scriptId() == selectedScript.scriptId()} ? ' table-primary'">
|
||||
<td><span class="badge text-bg-light" th:text="${item.category()}">ASO / 마스킹</span></td>
|
||||
<td><code th:text="${item.fileName()}">62_kb_aso_masking_backoffice_metadata.sql</code></td>
|
||||
<td>
|
||||
<strong th:text="${item.title()}">컬럼 마스킹 규칙 메타데이터</strong>
|
||||
<div class="form-hint" th:text="${item.description()}">설명</div>
|
||||
</td>
|
||||
<td><a class="btn btn-sm rw-btn-secondary" th:href="@{/security-sql-scripts(script=${item.scriptId()})}">원문 보기</a></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(scripts)}"><td colspan="4" class="text-muted">표시할 스크립트가 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band" th:if="${selectedScript}">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="badge text-bg-secondary" th:text="${selectedScript.category()}">ASO / 마스킹</span>
|
||||
<h2 class="mt-2" th:text="${selectedScript.title()}">컬럼 마스킹 규칙 메타데이터</h2>
|
||||
<p class="section-subtitle" th:text="${selectedScript.description()}">설명</p>
|
||||
</div>
|
||||
<code th:text="${selectedScript.fileName()}">62_kb_aso_masking_backoffice_metadata.sql</code>
|
||||
</div>
|
||||
<p class="form-hint">Git source: <code th:text="${'sql/adb/' + selectedScript.fileName()}">sql/adb/62_kb_aso_masking_backoffice_metadata.sql</code>. 실제 DB 배포본은 <a href="/vpd-filter-runtime">행 접근 필터 구조</a> 및 DB 배포 이력과 함께 확인하세요.</p>
|
||||
<form hx-post="/security-sql-scripts/explanation" hx-target="#security-sql-explanation" hx-swap="innerHTML" class="mb-3">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="script" th:value="${selectedScript.scriptId()}">
|
||||
<button class="btn rw-btn-primary" type="submit">LLM 전체·블록·토큰 처리 설명 생성</button>
|
||||
<span class="form-hint ms-2">원문은 실행·수정하지 않고 설명을 만들 때만 AI에 전달합니다.</span>
|
||||
</form>
|
||||
<section id="security-sql-explanation" class="mb-3"></section>
|
||||
<section class="probe-exchange"><pre th:text="${selectedScript.source()}">-- SQL source</pre></section>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
이 화면은 관리자용 원장 미리보기입니다. 사용자별 VPD 적용 결과는 <a href="/probe">접근 검증</a>에서 확인하세요.
|
||||
이 화면은 관리자용 원장 미리보기입니다. 사용자별 행 접근 적용 결과는 <a href="/probe">접근 검증</a>에서 확인하세요.
|
||||
</div>
|
||||
|
||||
<section class="content-band">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>검증 세션</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>이해당사자 원장에 등록된 업무 사용자를 토큰에 연결합니다. 토큰 원문은 한 번만 표시하며, DB에는 해시와 식별용 prefix만 보관합니다. 역할·채널·행 권한은 토큰이 아니라 접근 규칙에서 동적으로 계산합니다.</p>
|
||||
<p>이해당사자 원장에 등록된 업무 사용자를 토큰에 연결합니다. 토큰 원문은 한 번만 표시하며, DB에는 해시와 식별용 prefix만 보관합니다. 역할·채널·행 권한은 토큰이 아니라 행 접근 규칙에서 동적으로 계산합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>이해당사자 토큰 발급</h2>
|
||||
<p class="section-subtitle">모든 KB 이해당사자에게 토큰을 발급할 수 있습니다. 지점장·설계사 등의 실제 행 범위는 접근 규칙에 연결된 역할로 결정되므로, 토큰을 다시 만들지 않아도 권한 변경이 반영됩니다.</p>
|
||||
<p class="section-subtitle">모든 KB 이해당사자에게 토큰을 발급할 수 있습니다. 지점장·설계사 등의 실제 행 범위는 행 접근 규칙에 연결된 역할로 결정되므로, 토큰을 다시 만들지 않아도 권한 변경이 반영됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/tokens" class="form-grid">
|
||||
|
||||
109
src/main/resources/templates/user-masking-rules.html
Normal file
109
src/main/resources/templates/user-masking-rules.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('컬럼 원문 표시 허용 사용자')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>컬럼 원문 표시 허용 사용자</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>이 화면은 <a href="/masking-rules">컬럼 마스킹</a>에서 연결한 기본 컬럼 마스킹 규칙에 대해 <strong>원문 표시 허용(UNMASK)</strong> 사용자만 관리합니다. 이 목록에 없는 사용자는 행 접근 정책이 조회를 허용하더라도 해당 컬럼의 마스킹값을 받습니다.</p>
|
||||
<p>행 접근 여부는 변경하지 않습니다. 행 접근 권한이 없으면 행을 볼 수 없고, 행 접근 권한이 있으며 이 목록에 없으면 마스킹값을 봅니다. 행 접근 권한이 있고 이 목록에 있으면 그 허용 범위 안에서만 원문을 봅니다. 저장한 변경은 DBMS_REDACT가 읽는 세션 context에 반영됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>컬럼 원문 표시 허용 사용자 추가</h2>
|
||||
<p class="section-subtitle">먼저 컬럼 마스킹 화면에서 민감 컬럼에 활성 규칙을 연결하세요. 이 등록은 원문 표시만 허용하며 행 접근 권한은 부여하지 않습니다.</p>
|
||||
<form method="post" action="/user-masking-rules" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
원문 표시를 허용할 사용자
|
||||
<select class="form-select" name="userId" required>
|
||||
<option value="">선택하세요</option>
|
||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:attr="data-user-label=${user.username()}"
|
||||
th:text="${user.username() + ' (' + user.empNo() + ')'}">KB_VPD_ADMIN</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
원문 표시 허용 대상 컬럼
|
||||
<select class="form-select" name="columnId" required>
|
||||
<option value="">선택하세요</option>
|
||||
<option th:each="columnRule : ${columnRules}" th:value="${columnRule.columnId()}"
|
||||
th:attr="data-target=${columnRule.targetLabel()},
|
||||
data-template=${columnRule.template().label()},
|
||||
data-result=${columnRule.template().previewResult()},
|
||||
data-aso-function=${columnRule.template().asoFunction()},
|
||||
data-context=${'MR_' + columnRule.columnId()}"
|
||||
th:text="${columnRule.targetLabel() + ' · ' + columnRule.ruleLabel()}">KB_CUSTOMERS.RRN_MASKED</option>
|
||||
</select>
|
||||
</label>
|
||||
<div>
|
||||
<span class="form-label">적용 결과</span>
|
||||
<div class="form-control bg-light" aria-label="적용 결과">행 접근 정책이 허용한 행에서만 원문 표시 · UNMASK</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">컬럼 원문 표시 허용 등록</button>
|
||||
</form>
|
||||
|
||||
<div class="policy-preview mt-3" data-masking-exception-preview>
|
||||
<h3>선택한 컬럼의 실제 적용 결과</h3>
|
||||
<p class="section-description" data-masking-preview-empty>원문 표시 허용 대상 컬럼을 선택하면 일반 사용자와 허용 사용자의 반환값을 보여줍니다.</p>
|
||||
<dl data-masking-preview-details hidden>
|
||||
<div>
|
||||
<dt>대상 · DB 마스킹 방식</dt>
|
||||
<dd data-masking-preview-target>-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>행 접근 권한이 없는 사용자</dt>
|
||||
<dd>행 자체가 반환되지 않습니다.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>행 접근 허용 · 원문 표시 허용 없음</dt>
|
||||
<dd data-masking-preview-masked>-</dd>
|
||||
</div>
|
||||
<div class="policy-preview-emphasis">
|
||||
<dt>행 접근 허용 · 원문 표시 허용 사용자</dt>
|
||||
<dd data-masking-preview-unmasked>-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>DB 판정 기준</dt>
|
||||
<dd data-masking-preview-context>-</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>컬럼 원문 표시 허용 사용자 목록</h2>
|
||||
<p class="section-subtitle">목록에 없는 사용자는 행 접근 정책이 허용한 행에서 기본 마스킹값을 봅니다.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>사용자</th><th>대상 컬럼</th><th>기본 규칙</th><th>원문 표시 상태</th><th>상태</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="userRule : ${userRules}">
|
||||
<td th:text="${userRule.username()}">KB_VPD_ADMIN</td>
|
||||
<td><code th:text="${userRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
|
||||
<td th:text="${userRule.ruleLabel()}">주민번호 기본 마스킹 · 주민등록번호 부분 마스킹</td>
|
||||
<td><span class="badge" th:classappend="${userRule.unmasked()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${userRule.decisionLabel()}">원문 표시 예외</span></td>
|
||||
<td th:text="${userRule.activeYn()}">Y</td>
|
||||
<td>
|
||||
<form method="post" action="/user-masking-rules/delete" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="userId" th:value="${userRule.userId()}">
|
||||
<input type="hidden" name="columnId" th:value="${userRule.columnId()}">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">원문 표시 허용 해제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(userRules)}"><td colspan="6" class="text-muted">원문 표시 허용 사용자가 없습니다. 행 접근 정책이 허용한 사용자에게 기본 컬럼 마스킹 규칙이 적용됩니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -83,9 +83,9 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>접근 정책 설정</h2>
|
||||
<p class="section-subtitle">역할별 기술 태그 접근 규칙은 권한 관리에서 관리합니다.</p>
|
||||
<p class="section-subtitle">역할별 기술 태그 행 접근 규칙은 행 접근 규칙 화면에서 관리합니다.</p>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-primary" href="/permissions">권한 관리 열기</a>
|
||||
<a class="btn btn-sm rw-btn-primary" href="/permissions">행 접근 규칙 열기</a>
|
||||
</div>
|
||||
<div class="macro-micro-grid">
|
||||
<div>
|
||||
@@ -121,7 +121,7 @@ AND NOT (DENY TAG C OR DENY TAG D)</pre>
|
||||
<ol>
|
||||
<li>검색어를 같은 임베딩 방식으로 벡터화합니다.</li>
|
||||
<li>임시 Bearer Token으로 전용 ORDS Handler를 호출합니다.</li>
|
||||
<li>VPD가 TECH_TAG 권한에 맞지 않는 검색 단위를 먼저 제거합니다.</li>
|
||||
<li>행 접근 정책이 TECH_TAG 권한에 맞지 않는 검색 단위를 먼저 제거합니다.</li>
|
||||
<li>남은 검색 단위를 관련도 순으로 반환합니다.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<td colspan="4" class="policy-source-cell">
|
||||
<div th:id="${'filter-source-' + iter.index}" class="text-muted small">Source 보기를 누르면 현재 함수 내용을 표시합니다.</div>
|
||||
<div class="alert alert-light mt-3 mb-0" th:if="${function.permissionSystemDefault()}">
|
||||
이 함수는 권한체계의 핵심 실행 경로이므로 직접 수정할 수 없습니다. 권한 규칙을 변경하세요.
|
||||
이 함수는 권한체계의 핵심 실행 경로이므로 직접 수정할 수 없습니다. 행 접근 규칙을 변경하세요.
|
||||
</div>
|
||||
<form method="post" action="/vpd-filter-policies/filters" class="filter-edit-form mt-3"
|
||||
th:unless="${function.permissionSystemDefault()}">
|
||||
|
||||
126
src/main/resources/templates/vpd-filter-runtime.html
Normal file
126
src/main/resources/templates/vpd-filter-runtime.html
Normal file
@@ -0,0 +1,126 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('행 접근 필터 구조')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<section class="page-title">
|
||||
<span class="badge text-bg-primary">운영 기준</span>
|
||||
<h1 class="mt-2">행 접근 필터 구조</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>기본 동적 필터 <code>CB_AGENT_DOC_VPD_FILTER</code>의 연결 상태와 현재 DB에 배포된 함수를 읽기 전용으로 보여줍니다. 행 접근 변경은 <a href="/permissions">행 접근 규칙</a>에서 수행합니다.</p>
|
||||
<section class="content-band mt-3 mb-0">
|
||||
<h2>행 필터(VPD) 적용 흐름</h2>
|
||||
<p><strong>Oracle VPD 정책 함수는 Bearer Token을 직접 해석하지 않습니다.</strong> 앞 단계의 <code>cb_agent_ctx_pkg.set_user_by_bearer</code>가 만든 <code>CB_AGENT_CTX</code>를 읽어, 현재 SELECT에 붙일 WHERE predicate를 생성합니다.</p>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="vpd-help-flow-title vpd-help-flow-desc">
|
||||
<title id="vpd-help-flow-title">Bearer 토큰 기반 행 필터 적용 흐름</title>
|
||||
<desc id="vpd-help-flow-desc">토큰으로 설정된 사용자 컨텍스트를 행 접근 함수가 읽고 역할과 행 접근 규칙을 WHERE 조건으로 변환해, 허용된 행만 SELECT 결과에 남긴다.</desc>
|
||||
<defs>
|
||||
<marker id="flow-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z"/></marker>
|
||||
</defs>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M185 92 H225"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M405 92 H445"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M625 92 H665"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M845 92 H885"/>
|
||||
<path class="diagram-link diagram-link-secondary" marker-end="url(#flow-arrow)" d="M745 212 V144"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M977 136 V315"/>
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M900 136 V280 H755 V315"/>
|
||||
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">ORDS 요청</text></g>
|
||||
<g class="diagram-node"><rect x="225" y="48" width="180" height="88" rx="12"/><text x="315" y="76" class="diagram-node-title">토큰 검증·해석</text><text x="315" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="315" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
|
||||
<g class="diagram-node"><rect x="445" y="48" width="180" height="88" rx="12"/><text x="535" y="76" class="diagram-node-title">CB_AGENT_CTX</text><text x="535" y="103" class="diagram-node-detail">USER_ID · DEPT_CODE · EMP_NO</text><text x="535" y="120" class="diagram-node-note">STAKEHOLDER_USER_ID · CHANNEL</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="665" y="48" width="180" height="88" rx="12"/><text x="755" y="76" class="diagram-node-title">행 접근 함수 호출</text><text x="755" y="103" class="diagram-node-detail">CB_AGENT_DOC_VPD_FILTER</text><text x="755" y="120" class="diagram-node-note">p_object = 조회 대상</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="885" y="48" width="185" height="88" rx="12"/><text x="977" y="76" class="diagram-node-title">WHERE predicate</text><text x="977" y="103" class="diagram-node-detail">ALLOW / DENY 결합</text><text x="977" y="120" class="diagram-node-note">원래 SELECT에 자동 추가</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="590" y="212" width="310" height="72" rx="12"/><text x="745" y="239" class="diagram-node-title">역할·권한·규칙 메타데이터</text><text x="745" y="263" class="diagram-node-detail">cb_user_role · cb_group_role · cb_permission_rule</text></g>
|
||||
<g class="diagram-node"><rect x="665" y="315" width="180" height="64" rx="12"/><text x="755" y="341" class="diagram-node-title">ALLOW 없음 / USER_ID 없음</text><text x="755" y="363" class="diagram-node-detail">RETURN 1 = 0 · 행 없음</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="885" y="315" width="185" height="64" rx="12"/><text x="977" y="341" class="diagram-node-title">ALLOW 조건 충족</text><text x="977" y="363" class="diagram-node-detail">조건에 맞는 행만 반환</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="form-hint mb-0"><code>rule_type</code>별 WHERE 조각은 권한 내부에서 AND, ALLOW 권한끼리는 OR, DENY 권한은 최종적으로 <code>AND NOT (...)</code>으로 결합됩니다. ASO는 같은 context의 <code>MR_<column_id></code>로 <em>컬럼 표시</em>를 정하고, 행 접근 필터는 <code>USER_ID</code>·<code>DEPT_CODE</code>·<code>EMP_NO</code>·<code>STAKEHOLDER_*</code>로 <em>보이는 행</em>을 정합니다.</p>
|
||||
</section>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="alert alert-warning" th:if="${runtimeError}">
|
||||
<strong th:text="${runtimeError.title()}">DB 연결 설정이 필요합니다.</strong>
|
||||
<span th:text="${runtimeError.message()}">DB 연결을 확인할 수 없습니다.</span>
|
||||
</section>
|
||||
<section class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}">조회할 수 없습니다.</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>실행 흐름</h2>
|
||||
<div class="macro-micro-grid">
|
||||
<div>
|
||||
<h3>1. 신뢰 컨텍스트 설정</h3>
|
||||
<p>ORDS가 Bearer Token을 검증하고 <code>CB_AGENT_CTX</code>에 사용자·이해관계자·채널 정보를 설정합니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>2. 권한과 규칙 계산</h3>
|
||||
<p>직접 역할과 활성 그룹 역할의 SELECT 권한을 읽습니다. 한 권한의 규칙은 AND, 여러 ALLOW 권한은 OR로 결합합니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>3. DB WHERE 절 적용</h3>
|
||||
<p>Oracle VPD 정책이 반환 predicate를 원래 SELECT에 자동 적용합니다. 권한이 없으면 <code>1 = 0</code>으로 행 접근을 막는 것이 기준입니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-hint mt-3 mb-0">행 접근은 이 행 필터(VPD)가 담당합니다. 컬럼 마스킹과 원문 표시 허용 사용자는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction에서만 제어합니다.</p>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>현재 연결된 행 접근 정책(VPD)</h2>
|
||||
<p class="section-subtitle">기본 Filter를 실제로 호출하는 보호 객체 정책입니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary" th:text="${#lists.size(policies)}">0</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr><th>Object</th><th>Policy</th><th>Statement</th><th>상태</th><th>Filter</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="policy : ${policies}">
|
||||
<td><code th:text="${policy.objectDisplayName()}">POC_2.KB_CONTRACTS</code></td>
|
||||
<td><code th:text="${policy.policyName()}">KB_KB_CONTRACTS_ROW_POLICY</code></td>
|
||||
<td th:text="${policy.statementTypes()}">SELECT</td>
|
||||
<td><span class="badge" th:classappend="${policy.enabled() == 'YES'} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${policy.enabled() == 'YES'} ? '적용됨' : '중지됨'">적용됨</span></td>
|
||||
<td><code th:text="${policy.functionDisplayName()}">ADMIN.CB_AGENT_DOC_VPD_FILTER</code></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(policies)}">
|
||||
<td colspan="5" class="text-muted">기본 행 접근 Filter가 연결된 보호 객체를 찾지 못했습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>운영 점검 기준</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>점검 항목</th><th>확인 기준</th><th>조치 위치</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 ORDS에서 검증하고 요청 종료 시 컨텍스트를 정리합니다.</td><td>ORDS Handler / <code>CB_AGENT_CTX_PKG</code></td></tr>
|
||||
<tr><td>권한 결합</td><td>권한 내부 규칙은 AND, ALLOW 권한은 OR, DENY 조건은 최종적으로 제외합니다.</td><td><a href="/permissions">행 접근 규칙</a></td></tr>
|
||||
<tr><td>오류·미권한</td><td>유효한 컨텍스트나 ALLOW 권한이 없으면 행 접근은 차단돼야 합니다.</td><td><a href="/probe">접근 검증</a></td></tr>
|
||||
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 보험료·주민번호 등의 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>배포된 함수 소스</h2>
|
||||
<p class="section-subtitle">현재 DB의 <code>ALL_SOURCE</code>에서 읽은 소스입니다. 형상 기준 SQL과 다르면 배포 이력을 확인하세요.</p>
|
||||
<section class="probe-exchange" th:if="${source}">
|
||||
<h3><span th:text="${source.owner() + '.' + source.objectName()}">ADMIN.CB_AGENT_DOC_VPD_FILTER</span> <span class="text-muted" th:text="${' / ' + source.objectType()}"> / FUNCTION</span></h3>
|
||||
<pre th:if="${source.found()}" th:text="${source.source()}"></pre>
|
||||
<pre th:unless="${source.found()}">ALL_SOURCE에서 조회 가능한 소스가 없습니다. DB 연결 계정의 source 조회 권한을 확인하세요.</pre>
|
||||
</section>
|
||||
<div th:unless="${source}" class="text-muted">기본 Filter source를 조회할 연결 정보를 찾지 못했습니다.</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,8 @@
|
||||
<h1>보호 상태</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>권한 화면에서 만든 사용자·그룹·역할·행·열 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다. Policy는 어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지 정하고, 실제 허용 조건은 권한 규칙과 Filter가 계산합니다.</p>
|
||||
<p>행 접근 규칙 화면에서 만든 사용자·그룹·역할·행 규칙을 Oracle VPD가 실제 TABLE/VIEW에 적용하도록 연결합니다. Policy는 어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지 정하고, 실제 허용 조건은 행 접근 규칙과 Filter가 계산합니다.</p>
|
||||
<p>이 화면은 <strong>행 접근 정책</strong>만 확인합니다. 컬럼 원문/마스킹 적용 상태는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 상태에서 확인하세요.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -23,10 +24,10 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>보호 대상</h2>
|
||||
<p class="section-subtitle">VPD 적용과 검증 준비 상태를 확인합니다.</p>
|
||||
<p class="section-subtitle">행 접근 정책 적용과 검증 준비 상태를 확인합니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>객체 상태 항목 설명 보기</summary>
|
||||
<p>VPD가 붙었는지, 권한 규칙과 검증 경로가 준비됐는지 한곳에서 확인합니다. 아래 기본 적용에서는 객체만 선택하면 됩니다.</p>
|
||||
<p>Oracle VPD 정책이 붙었는지, 행 접근 규칙과 검증 경로가 준비됐는지 한곳에서 확인합니다. 아래 기본 적용에서는 객체만 선택하면 됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary" th:text="${#lists.size(vpdTargets)}">0</span>
|
||||
|
||||
Reference in New Issue
Block a user