package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.ItDlpPolicy; import com.kaidi.oa.repository.ItDlpPolicyRepository; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 信息部·网络与信息安全·DLP(数据防泄漏)策略规则台账(需求 §4 数据安全·DLP策略管理)。 * *

可配置字段级脱敏规则与自动拦截策略台账,IT 管理员在此维护 DLP 规则条目并对接 * 数据导出审批(ItDataExportController)形成完整 DLP 管控闭环。 * *

功能点: *

* *

读口含策略明细(含安全拦截规则,属安全机密),见 AuthInterceptor SENSITIVE_READ_PREFIXES(需追加)。 * 写口默认受 default-deny(ADMIN/APPROVER) 保护。 */ @RestController @RequestMapping("/api/oa/it-dlp-policies") public class ItDlpPolicyController { private final ItDlpPolicyRepository repo; public ItDlpPolicyController(ItDlpPolicyRepository repo) { this.repo = repo; } // ---------- 列表 / 详情 ---------- @GetMapping public ApiResp> list( @RequestParam(required = false) String status, @RequestParam(required = false) String policyType, @RequestParam(required = false) String scope) { if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (policyType != null && !policyType.isBlank()) { return ApiResp.ok(repo.findByPolicyType(policyType)); } if (scope != null && !scope.isBlank()) { return ApiResp.ok(repo.findByScope(scope)); } return ApiResp.ok(repo.findAll()); } /** 获取当前所有启用规则(按优先级升序,优先级1最高)。 */ @GetMapping("/active") public ApiResp> active() { return ApiResp.ok(repo.findByStatusOrderByPriorityAsc("启用")); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(require(id)); } // ---------- 创建 / 编辑 ---------- public record PolicyRequest( String policyName, String policyType, String scope, String triggerCondition, String ruleDetail, String affectedFields, Integer priority, String actionOnHit, String createdBy, String effectiveDate, String expiryDate, String remark) { } /** 新建 DLP 策略(置草稿,待审批后启用)。 */ @PostMapping public ApiResp create(@RequestBody PolicyRequest req) { if (req.policyName() == null || req.policyName().isBlank()) { throw new ApiException(400, "策略名称(policyName) 不能为空"); } if (req.ruleDetail() == null || req.ruleDetail().isBlank()) { throw new ApiException(400, "规则描述(ruleDetail) 不能为空"); } Instant now = Instant.now(); ItDlpPolicy p = new ItDlpPolicy(); p.setPolicyNo("DLP-" + LocalDate.now().toString().replace("-", "") + "-" + String.format("%03d", (repo.count() + 1) % 1000)); p.setPolicyName(req.policyName()); p.setPolicyType(req.policyType() == null ? "字段脱敏" : req.policyType()); p.setScope(req.scope() == null ? "全局" : req.scope()); p.setTriggerCondition(req.triggerCondition()); p.setRuleDetail(req.ruleDetail()); p.setAffectedFields(req.affectedFields()); p.setPriority(req.priority() == null ? 5 : req.priority()); p.setActionOnHit(req.actionOnHit() == null ? "记录日志" : req.actionOnHit()); p.setStatus("草稿"); p.setCreatedBy(req.createdBy()); p.setEffectiveDate(req.effectiveDate() == null ? LocalDate.now().toString() : req.effectiveDate()); p.setExpiryDate(req.expiryDate()); p.setRemark(req.remark()); p.setHitCount(0); p.setCreatedAt(now); p.setUpdatedAt(now); return ApiResp.ok(repo.save(p)); } @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody PolicyRequest req) { ItDlpPolicy p = require(id); if ("启用".equals(p.getStatus())) { throw new ApiException(409, "启用中的策略不可直接编辑,请先停用"); } if (req.policyName() != null && !req.policyName().isBlank()) p.setPolicyName(req.policyName()); if (req.policyType() != null) p.setPolicyType(req.policyType()); if (req.scope() != null) p.setScope(req.scope()); if (req.triggerCondition() != null) p.setTriggerCondition(req.triggerCondition()); if (req.ruleDetail() != null && !req.ruleDetail().isBlank()) p.setRuleDetail(req.ruleDetail()); if (req.affectedFields() != null) p.setAffectedFields(req.affectedFields()); if (req.priority() != null) p.setPriority(req.priority()); if (req.actionOnHit() != null) p.setActionOnHit(req.actionOnHit()); if (req.effectiveDate() != null) p.setEffectiveDate(req.effectiveDate()); if (req.expiryDate() != null) p.setExpiryDate(req.expiryDate()); if (req.remark() != null) p.setRemark(req.remark()); p.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(p)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { ItDlpPolicy p = require(id); if ("启用".equals(p.getStatus())) { throw new ApiException(409, "启用中的策略不可删除,请先停用"); } repo.deleteById(id); return ApiResp.ok(null); } // ---------- 状态机 ---------- public record EnableRequest(String approvedBy) { } /** 启用策略:草稿/停用 → 启用(须指定 IT 主管审批人)。 */ @Transactional @PostMapping("/{id}/enable") public ApiResp enable(@PathVariable Long id, @RequestBody EnableRequest req) { ItDlpPolicy p = require(id); if ("启用".equals(p.getStatus())) { throw new ApiException(409, "策略已是启用状态"); } if (req.approvedBy() == null || req.approvedBy().isBlank()) { throw new ApiException(400, "启用须指定审批人(approvedBy)"); } p.setApprovedBy(req.approvedBy()); p.setStatus("启用"); p.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(p)); } /** 停用策略:启用 → 停用。 */ @PostMapping("/{id}/disable") public ApiResp disable(@PathVariable Long id) { ItDlpPolicy p = require(id); if (!"启用".equals(p.getStatus())) { throw new ApiException(409, "仅「启用」策略可停用,当前:" + p.getStatus()); } p.setStatus("停用"); p.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(p)); } /** 命中记录(内部回调):自增 hitCount、记录最近命中时刻(联动数据导出审批流程)。 */ @Transactional @PostMapping("/{id}/hit") public ApiResp hit(@PathVariable Long id) { ItDlpPolicy p = require(id); if (!"启用".equals(p.getStatus())) { throw new ApiException(409, "仅「启用」策略可记录命中"); } p.setHitCount((p.getHitCount() == null ? 0 : p.getHitCount()) + 1); p.setLastHitAt(Instant.now()); p.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(p)); } // ---------- 统计看板 ---------- public record PolicyStats( int total, int active, int draft, int disabled, Map byType, Map byScope, List topHit) { } /** DLP 策略统计看板:总数/启用/草稿/停用、按类型/范围分布、命中次数 TOP5。 */ @GetMapping("/stats") public ApiResp stats() { List all = repo.findAll(); int active = 0, draft = 0, disabled = 0; Map byType = new LinkedHashMap<>(); Map byScope = new LinkedHashMap<>(); for (ItDlpPolicy p : all) { switch (p.getStatus() == null ? "草稿" : p.getStatus()) { case "启用" -> active++; case "草稿" -> draft++; case "停用" -> disabled++; } if (p.getPolicyType() != null) { byType.merge(p.getPolicyType(), 1, Integer::sum); } if (p.getScope() != null) { byScope.merge(p.getScope(), 1, Integer::sum); } } // 命中 TOP5(按 hitCount 降序) List topHit = all.stream() .filter(p -> p.getHitCount() != null && p.getHitCount() > 0) .sorted((a, b) -> Integer.compare( b.getHitCount() == null ? 0 : b.getHitCount(), a.getHitCount() == null ? 0 : a.getHitCount())) .limit(5) .toList(); return ApiResp.ok(new PolicyStats(all.size(), active, draft, disabled, byType, byScope, topHit)); } // ---------- helpers ---------- private ItDlpPolicy require(Long id) { return repo.findById(id) .orElseThrow(() -> new NotFoundException("DLP policy not found: " + id)); } }