Consolidate data access control backoffice updates
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user