fix #477: apply vpd by policy filter template
This commit is contained in:
@@ -7,6 +7,7 @@ public record VpdPolicyFormOptions(
|
|||||||
List<String> schemaOwners,
|
List<String> schemaOwners,
|
||||||
List<String> owners,
|
List<String> owners,
|
||||||
List<VpdFunctionOption> functions,
|
List<VpdFunctionOption> functions,
|
||||||
|
List<VpdPolicyTemplateOption> policyTemplates,
|
||||||
List<String> statementTypes
|
List<String> statementTypes
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||||
|
|
||||||
|
public record VpdPolicyTemplateOption(
|
||||||
|
String policyName,
|
||||||
|
String functionOwner,
|
||||||
|
String packageName,
|
||||||
|
String functionName,
|
||||||
|
String statementTypes,
|
||||||
|
String enabled,
|
||||||
|
String checkOption
|
||||||
|
) {
|
||||||
|
|
||||||
|
public String functionKey() {
|
||||||
|
String packagePrefix = packageName == null || packageName.isBlank() ? "" : packageName + ".";
|
||||||
|
return functionOwner + "." + packagePrefix + functionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String filterDisplayName() {
|
||||||
|
return functionKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String label() {
|
||||||
|
return policyName + " / " + filterDisplayName() + " / " + statementTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean enabledValue() {
|
||||||
|
return "YES".equalsIgnoreCase(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean updateCheckValue() {
|
||||||
|
return "YES".equalsIgnoreCase(checkOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.cloudhandson.vpdbackoffice.mapper;
|
package com.cloudhandson.vpdbackoffice.mapper;
|
||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyTemplateOption;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdTargetView;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdTargetView;
|
||||||
@@ -23,6 +24,8 @@ public interface VpdPolicyMapper {
|
|||||||
|
|
||||||
List<VpdFunctionOption> findFunctionOptions();
|
List<VpdFunctionOption> findFunctionOptions();
|
||||||
|
|
||||||
|
List<VpdPolicyTemplateOption> findPolicyTemplateOptions();
|
||||||
|
|
||||||
List<VpdSchemaObjectOption> findSchemaObjects(
|
List<VpdSchemaObjectOption> findSchemaObjects(
|
||||||
@Param("owner") String owner,
|
@Param("owner") String owner,
|
||||||
@Param("includeTablesYn") String includeTablesYn,
|
@Param("includeTablesYn") String includeTablesYn,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public class VpdPolicyService {
|
|||||||
mapper.findSchemaOwnerOptions(),
|
mapper.findSchemaOwnerOptions(),
|
||||||
mapper.findOwnerOptions(),
|
mapper.findOwnerOptions(),
|
||||||
mapper.findFunctionOptions(),
|
mapper.findFunctionOptions(),
|
||||||
|
mapper.findPolicyTemplateOptions(),
|
||||||
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
|
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
|
||||||
);
|
);
|
||||||
formOptionsCache = new CacheEntry<>(options, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
|
formOptionsCache = new CacheEntry<>(options, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
|
||||||
@@ -81,6 +82,7 @@ public class VpdPolicyService {
|
|||||||
List.of(),
|
List.of(),
|
||||||
List.of(),
|
List.of(),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
List.of(),
|
||||||
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
|
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,45 @@
|
|||||||
FETCH FIRST 300 ROWS ONLY
|
FETCH FIRST 300 ROWS ONLY
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="findPolicyTemplateOptions" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyTemplateOption">
|
||||||
|
SELECT policy_name,
|
||||||
|
function_owner,
|
||||||
|
package_name,
|
||||||
|
function_name,
|
||||||
|
statement_types,
|
||||||
|
enabled,
|
||||||
|
check_option
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT
|
||||||
|
p.policy_name,
|
||||||
|
p.pf_owner AS function_owner,
|
||||||
|
p.package AS package_name,
|
||||||
|
p.function AS function_name,
|
||||||
|
RTRIM(
|
||||||
|
CASE WHEN p.sel = 'YES' THEN 'SELECT,' ELSE '' END ||
|
||||||
|
CASE WHEN p.ins = 'YES' THEN 'INSERT,' ELSE '' END ||
|
||||||
|
CASE WHEN p.upd = 'YES' THEN 'UPDATE,' ELSE '' END ||
|
||||||
|
CASE WHEN p.del = 'YES' THEN 'DELETE,' ELSE '' END ||
|
||||||
|
CASE WHEN p.idx = 'YES' THEN 'INDEX,' ELSE '' END,
|
||||||
|
','
|
||||||
|
) AS statement_types,
|
||||||
|
p.enable AS enabled,
|
||||||
|
p.chk_option AS check_option
|
||||||
|
FROM all_policies p
|
||||||
|
WHERE p.policy_name IS NOT NULL
|
||||||
|
AND p.pf_owner IS NOT NULL
|
||||||
|
AND p.function IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM cb_protected_object o
|
||||||
|
WHERE o.enabled_yn = 'Y'
|
||||||
|
AND o.owner = p.object_owner
|
||||||
|
AND o.object_name = p.object_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY policy_name, function_owner, package_name, function_name
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="findSchemaObjects" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption">
|
<select id="findSchemaObjects" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption">
|
||||||
SELECT owner, object_name, object_type
|
SELECT owner, object_name, object_type
|
||||||
FROM all_objects
|
FROM all_objects
|
||||||
|
|||||||
@@ -28,6 +28,37 @@ function findHtmxTarget(button) {
|
|||||||
return selector ? document.querySelector(selector) : null;
|
return selector ? document.querySelector(selector) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPolicyTemplate(select) {
|
||||||
|
const form = select?.closest('form');
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const option = select.options[select.selectedIndex];
|
||||||
|
const policyName = form.querySelector('[data-policy-template-field="policyName"]');
|
||||||
|
const functionKey = form.querySelector('[data-policy-template-field="functionKey"]');
|
||||||
|
const enabled = form.querySelector('[data-policy-template-field="enabled"]');
|
||||||
|
const updateCheck = form.querySelector('[data-policy-template-field="updateCheck"]');
|
||||||
|
if (policyName) {
|
||||||
|
policyName.value = option?.dataset.policyName || '';
|
||||||
|
}
|
||||||
|
if (functionKey) {
|
||||||
|
functionKey.value = option?.dataset.functionKey || '';
|
||||||
|
}
|
||||||
|
if (enabled) {
|
||||||
|
enabled.checked = option?.dataset.enabled === 'true';
|
||||||
|
}
|
||||||
|
if (updateCheck) {
|
||||||
|
updateCheck.checked = option?.dataset.updateCheck === 'true';
|
||||||
|
}
|
||||||
|
const selectedStatements = (option?.dataset.statementTypes || '')
|
||||||
|
.split(',')
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
form.querySelectorAll('[data-policy-template-statement]').forEach((checkbox) => {
|
||||||
|
checkbox.checked = selectedStatements.includes(checkbox.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return value
|
return value
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
@@ -423,6 +454,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (catalog) {
|
if (catalog) {
|
||||||
catalog.addEventListener('change', syncObjectCatalogSelection);
|
catalog.addEventListener('change', syncObjectCatalogSelection);
|
||||||
}
|
}
|
||||||
|
document.querySelectorAll('[data-policy-template-select]').forEach((select) => {
|
||||||
|
select.addEventListener('change', () => syncPolicyTemplate(select));
|
||||||
|
syncPolicyTemplate(select);
|
||||||
|
});
|
||||||
document.querySelectorAll('[data-rule-add]').forEach((button) => {
|
document.querySelectorAll('[data-rule-add]').forEach((button) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const row = button.closest('.rule-row');
|
const row = button.closest('.rule-row');
|
||||||
|
|||||||
@@ -136,72 +136,46 @@
|
|||||||
th:text="${target.objectDisplayName() + ' / ' + target.objectType() + (target.vpdApplied() ? ' / VPD 적용됨' : ' / 미적용')}"></option>
|
th:text="${target.objectDisplayName() + ' / ' + target.objectType() + (target.vpdApplied() ? ' / VPD 적용됨' : ' / 미적용')}"></option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
|
||||||
Policy 이름
|
|
||||||
<input class="form-control" name="policyName" list="vpd-policy-names"
|
|
||||||
placeholder="예: CB_AGENT_DOC_POLICY" required>
|
|
||||||
</label>
|
|
||||||
<label class="span-2">
|
<label class="span-2">
|
||||||
VPD Filter Function
|
적용할 Policy / Filter
|
||||||
<select class="form-select" name="functionKey">
|
<select class="form-select" data-policy-template-select required>
|
||||||
<option value="">기본 권한 함수가 없으면 Filter predicate로 새 function 자동 생성</option>
|
<option value="">등록된 policy/filter 조합 선택</option>
|
||||||
<option th:each="function : ${formOptions.functions()}"
|
<option th:each="template : ${formOptions.policyTemplates()}"
|
||||||
th:value="${function.value()}"
|
th:value="${template.label()}"
|
||||||
th:text="${function.functionName() == 'CB_AGENT_DOC_VPD_FILTER' ? function.label() + ' / 기본 권한 테이블 필터' : function.label()}"
|
th:data-policy-name="${template.policyName()}"
|
||||||
th:selected="${function.value() == formOptions.defaultPermissionFunctionKey()}"></option>
|
th:data-function-key="${template.functionKey()}"
|
||||||
|
th:data-statement-types="${template.statementTypes()}"
|
||||||
|
th:data-enabled="${template.enabledValue()}"
|
||||||
|
th:data-update-check="${template.updateCheckValue()}"
|
||||||
|
th:text="${template.label()}"></option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<input type="hidden" name="policyName" data-policy-template-field="policyName">
|
||||||
Function Owner
|
<input type="hidden" name="functionKey" data-policy-template-field="functionKey">
|
||||||
<select class="form-select" name="functionOwner">
|
|
||||||
<option value="">현재 연결 사용자</option>
|
|
||||||
<option th:each="owner : ${formOptions.owners()}"
|
|
||||||
th:value="${owner}"
|
|
||||||
th:text="${owner}"></option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Function 이름
|
|
||||||
<input class="form-control" name="functionName" list="vpd-function-names"
|
|
||||||
placeholder="POLICY_NAME_FILTER 자동 생성">
|
|
||||||
</label>
|
|
||||||
<div>
|
<div>
|
||||||
Statement Types
|
Statement Types
|
||||||
<div class="checkbox-row">
|
<div class="checkbox-row">
|
||||||
<label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
|
<label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
|
||||||
<input class="form-check-input" type="checkbox" name="statementTypes"
|
<input class="form-check-input" type="checkbox" name="statementTypes"
|
||||||
th:value="${statement}" th:checked="${statement == 'SELECT'}">
|
th:value="${statement}" th:checked="${statement == 'SELECT'}"
|
||||||
|
data-policy-template-statement>
|
||||||
<span class="form-check-label" th:text="${statement}">SELECT</span>
|
<span class="form-check-label" th:text="${statement}">SELECT</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check align-self-end">
|
<div class="form-check align-self-end">
|
||||||
<input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked>
|
<input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked
|
||||||
|
data-policy-template-field="enabled">
|
||||||
<label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label>
|
<label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check span-2">
|
<div class="form-check span-2">
|
||||||
<input class="form-check-input" id="vpd-update-check" type="checkbox" name="updateCheck" value="true">
|
<input class="form-check-input" id="vpd-update-check" type="checkbox" name="updateCheck" value="true"
|
||||||
|
data-policy-template-field="updateCheck">
|
||||||
<label class="form-check-label" for="vpd-update-check">INSERT/UPDATE에도 predicate check 적용</label>
|
<label class="form-check-label" for="vpd-update-check">INSERT/UPDATE에도 predicate check 적용</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="span-2">
|
<p class="text-muted small span-2 mb-0">Policy/Filter 조합은 Filter Policy 관리에서 등록/수정합니다. 여기서는 선택한 조합을 대상 TABLE/VIEW에 적용합니다.</p>
|
||||||
Filter predicate
|
|
||||||
<textarea class="form-control" id="vpd-filter-predicate" name="filterPredicate" rows="4"
|
|
||||||
placeholder="예: dept_code = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'') 비우면 기존 Function 이름으로 ADD_POLICY만 실행합니다."></textarea>
|
|
||||||
</label>
|
|
||||||
<div class="question-presets span-2" aria-label="Filter predicate 예시">
|
|
||||||
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="1=0">전체 차단</button>
|
|
||||||
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="1=1">전체 허용</button>
|
|
||||||
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="dept_code = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE')">부서 일치</button>
|
|
||||||
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="owner_emp_no = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')">본인 소유</button>
|
|
||||||
</div>
|
|
||||||
<button class="btn rw-btn-primary" type="submit">개별 객체에 VPD 적용</button>
|
<button class="btn rw-btn-primary" type="submit">개별 객체에 VPD 적용</button>
|
||||||
</form>
|
</form>
|
||||||
<datalist id="vpd-policy-names">
|
|
||||||
<option th:each="policyName : ${formOptions.policyNames()}" th:value="${policyName}"></option>
|
|
||||||
</datalist>
|
|
||||||
<datalist id="vpd-function-names">
|
|
||||||
<option th:each="function : ${formOptions.functions()}" th:value="${function.functionName()}"></option>
|
|
||||||
</datalist>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-band">
|
<section class="content-band">
|
||||||
|
|||||||
Reference in New Issue
Block a user