Consolidate data access control backoffice updates

This commit is contained in:
devmrko
2026-07-13 23:06:23 +09:00
parent 403298d474
commit e18b30feab
181 changed files with 11571 additions and 954 deletions

View File

@@ -0,0 +1,223 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRuleCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingTemplate;
import com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper;
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MaskingRuleService {
private static final Pattern RULE_CODE = Pattern.compile("[A-Z][A-Z0-9_]{2,63}");
private static final Set<String> DECISIONS = Set.of("MASK", "UNMASK");
private final MaskingRuleMapper mapper;
private final UserMapper userMapper;
private final ProtectedObjectService protectedObjectService;
private final AuditService auditService;
private final MaskingPolicySynchronizer maskingPolicySynchronizer;
public MaskingRuleService(
MaskingRuleMapper mapper,
UserMapper userMapper,
ProtectedObjectService protectedObjectService,
AuditService auditService,
MaskingPolicySynchronizer maskingPolicySynchronizer
) {
this.mapper = mapper;
this.userMapper = userMapper;
this.protectedObjectService = protectedObjectService;
this.auditService = auditService;
this.maskingPolicySynchronizer = maskingPolicySynchronizer;
}
public List<MaskingRule> findAllRules() {
return mapper.findAllRules();
}
public List<MaskingRule> findEnabledRules() {
return mapper.findEnabledRules();
}
public List<ColumnMaskingRule> findColumnRules() {
return mapper.findColumnRules();
}
/** Reads the actual Oracle Data Redaction state for the three managed KB objects. */
public List<MaskingPolicyStatus> findPolicyStatuses() {
return mapper.findPolicyStatuses();
}
public Set<String> managedObjectNames() {
return maskingPolicySynchronizer.managedObjectNames();
}
public List<UserMaskingRule> findUserRules() {
return mapper.findUserRules();
}
@Transactional
public void createRule(MaskingRuleCreateCommand command) {
String code = normalizeCode(command.ruleCode());
String name = normalizeRequired(command.ruleName(), 100, "규칙명");
String templateCode = normalizeTemplate(command.templateCode());
String description = normalizeOptional(command.description(), 400, "설명");
if (mapper.findRuleByCode(code) != null) {
throw new AppException("이미 등록된 컬럼 마스킹 규칙 코드입니다: " + code);
}
long ruleId = mapper.nextRuleId();
mapper.insertRule(ruleId, new MaskingRuleCreateCommand(code, name, templateCode, description));
auditService.record(new AuditEvent("MASKING_RULE_CREATED", null, null, "SUCCESS", null, null,
"ruleId=" + ruleId + ",code=" + code + ",template=" + templateCode));
}
@Transactional
public MaskingPolicySynchronizer.MaskingPolicySyncResult setRuleActive(long ruleId, boolean active) {
if (mapper.updateRuleActive(ruleId, active ? "Y" : "N") == 0) {
throw new AppException("컬럼 마스킹 규칙을 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("MASKING_RULE_ACTIVE_CHANGED", null, null, "SUCCESS", null, null,
"ruleId=" + ruleId + ",active=" + active));
return synchronizeDatabasePolicies();
}
@Transactional
public MaskingPolicySynchronizer.MaskingPolicySyncResult assignRuleToColumn(long columnId, long ruleId) {
var column = protectedObjectService.findColumn(columnId);
if (column == null) {
throw new AppException("보호 컬럼을 찾을 수 없습니다.");
}
if (!column.sensitive()) {
throw new AppException("민감 표시 보호 컬럼에만 컬럼 마스킹 규칙을 연결할 수 있습니다.");
}
MaskingRule rule = requireEnabledRule(ruleId);
mapper.upsertColumnRule(columnId, ruleId);
auditService.record(new AuditEvent("COLUMN_MASKING_RULE_ASSIGNED", null, column.objectId(), "SUCCESS", null, null,
"columnId=" + columnId + ",rule=" + rule.ruleCode()));
return synchronizeDatabasePolicies();
}
@Transactional
public MaskingPolicySynchronizer.MaskingPolicySyncResult addTargetColumn(long objectId, String columnName) {
ProtectedObject object = protectedObjectService.assertEnabled(objectId);
if (!maskingPolicySynchronizer.isManagedObject(object.objectName())) {
throw new AppException("ASO 마스킹 정책 관리 대상 객체가 아닙니다: " + object.objectName());
}
var column = protectedObjectService.addSensitiveColumnTarget(objectId, columnName);
auditService.record(new AuditEvent("MASKING_TARGET_COLUMN_REGISTERED", null, objectId, "SUCCESS", null, null,
object.objectName() + "." + column.columnName()));
return synchronizeDatabasePolicies();
}
@Transactional
public MaskingPolicySynchronizer.MaskingPolicySyncResult removeRuleFromColumn(long columnId) {
mapper.deleteUserRulesForColumn(columnId);
if (mapper.deleteColumnRule(columnId) == 0) {
throw new AppException("해제할 컬럼 마스킹 규칙을 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("COLUMN_MASKING_RULE_REMOVED", null, null, "SUCCESS", null, null,
"columnId=" + columnId));
return synchronizeDatabasePolicies();
}
/**
* Reconciles the current metadata with Oracle Data Redaction. This is exposed for the one-time
* repair of settings saved before automatic synchronization was introduced.
*/
@Transactional
public MaskingPolicySynchronizer.MaskingPolicySyncResult synchronizeDatabasePolicies() {
MaskingPolicySynchronizer.MaskingPolicySyncResult result = maskingPolicySynchronizer.synchronize();
auditService.record(new AuditEvent("MASKING_POLICY_SYNCHRONIZED", null, null, "SUCCESS", null, null,
"disabled=" + result.disabledPolicies() + ",enabled=" + result.enabledPolicies()
+ ",added=" + result.addedColumns() + ",modified=" + result.modifiedColumns()
+ ",dropped=" + result.droppedColumns()));
return result;
}
@Transactional
public void assignUserRule(long userId, long columnId, String decision) {
if (userMapper.findById(userId) == null) {
throw new AppException("사용자를 찾을 수 없습니다.");
}
ColumnMaskingRule columnRule = mapper.findColumnRule(columnId);
if (columnRule == null || !columnRule.ruleEnabled()) {
throw new AppException("먼저 활성 컬럼 마스킹 규칙을 민감 컬럼에 연결하세요.");
}
String normalizedDecision = normalizeDecision(decision);
mapper.upsertUserRule(userId, columnId, normalizedDecision);
auditService.record(new AuditEvent("USER_MASKING_RULE_ASSIGNED", userId, columnRule.objectId(), "SUCCESS", null, null,
"columnId=" + columnId + ",decision=" + normalizedDecision));
}
@Transactional
public void removeUserRule(long userId, long columnId) {
if (mapper.deleteUserRule(userId, columnId) == 0) {
throw new AppException("해제할 사용자별 컬럼 마스킹 규칙을 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("USER_MASKING_RULE_REMOVED", userId, null, "SUCCESS", null, null,
"columnId=" + columnId));
}
private MaskingRule requireEnabledRule(long ruleId) {
MaskingRule rule = mapper.findRuleById(ruleId);
if (rule == null || !rule.enabled()) {
throw new AppException("활성 컬럼 마스킹 규칙을 선택하세요.");
}
return rule;
}
private String normalizeCode(String value) {
String normalized = normalizeRequired(value, 64, "규칙 코드").toUpperCase(Locale.ROOT);
if (!RULE_CODE.matcher(normalized).matches()) {
throw new AppException("규칙 코드는 영문 대문자·숫자·밑줄로 3~64자여야 합니다.");
}
return normalized;
}
private String normalizeTemplate(String value) {
try {
return MaskingTemplate.from(normalizeRequired(value, 30, "마스킹 템플릿")).code();
} catch (IllegalArgumentException exception) {
throw new AppException(exception.getMessage());
}
}
private String normalizeDecision(String value) {
String normalized = normalizeRequired(value, 10, "사용자별 적용 방식").toUpperCase(Locale.ROOT);
if (!DECISIONS.contains(normalized)) {
throw new AppException("사용자별 적용 방식은 MASK 또는 UNMASK만 가능합니다.");
}
return normalized;
}
private String normalizeRequired(String value, int maxLength, String label) {
String normalized = value == null ? "" : value.trim();
if (normalized.isEmpty()) {
throw new AppException(label + "은(는) 필수입니다.");
}
if (normalized.length() > maxLength) {
throw new AppException(label + "은(는) " + maxLength + "자 이내여야 합니다.");
}
return normalized;
}
private String normalizeOptional(String value, int maxLength, String label) {
String normalized = value == null ? "" : value.trim();
if (normalized.length() > maxLength) {
throw new AppException(label + "은(는) " + maxLength + "자 이내여야 합니다.");
}
return normalized.isEmpty() ? null : normalized;
}
}