diff --git a/docs/design/467-column-sensitivity-redaction/README.md b/docs/design/467-column-sensitivity-redaction/README.md new file mode 100644 index 0000000..86e45d9 --- /dev/null +++ b/docs/design/467-column-sensitivity-redaction/README.md @@ -0,0 +1,35 @@ +# #467 컬럼 민감도와 redaction 정책 분리 + +## 배경 + +기존 모델은 `cb_permission_column`에 포함된 컬럼을 "NULL 처리 예외"로 해석했다. 이 방식은 역할별 표시 예외는 표현할 수 있지만, 컬럼 자체의 데이터 민감도와 masking 방식을 별도로 관리하기 어렵다. + +## 결정 + +- `cb_protected_column`을 컬럼 정책의 기준 테이블로 유지한다. +- `sensitivity_level`을 추가해 컬럼 데이터 등급을 관리한다. + - `PUBLIC` + - `INTERNAL` + - `CONFIDENTIAL` + - `RESTRICTED` +- `redaction_method`를 추가해 masking 방식을 관리한다. + - `NONE` + - `NULLIFY` + - `PARTIAL` + - `FULL` +- `cb_app_role.max_sensitivity_level`을 추가해 역할별 민감도 허용 상한을 설정한다. +- `cb_permission_column`은 기존 호환을 위해 "해당 권한에서 redaction 예외로 표시 가능한 컬럼" 의미로 유지한다. + +## Migration + +기존 `sensitive_yn = 'Y'` 컬럼은 `CONFIDENTIAL/NULLIFY`로 보정한다. 기존 `sensitive_yn = 'N'` 컬럼은 `PUBLIC/NONE`을 기본값으로 둔다. 운영자가 실제 업무 기준에 따라 `INTERNAL/NONE`, `RESTRICTED/FULL` 등으로 조정한다. + +## UI 반영 + +- ORDS 조회 Handler 대상 화면에서 객체별 컬럼 정책을 조회/수정한다. +- 역할 화면에서 `max_sensitivity_level`을 조회/수정한다. +- ORDS 검증 결과의 masked 컬럼은 `contents [CONFIDENTIAL/NULLIFY]`처럼 정책 라벨을 같이 표시한다. + +## 남은 판단 + +`cb_permission_column`과 `max_sensitivity_level`의 최종 런타임 결합 방식은 다음 단계에서 결정한다. 현재는 기존 Oracle Redaction expression과 호환되도록 표시 예외 구조를 유지한다. diff --git a/sql/adb/17_agent_ords_security_local_vpd_setup.sql b/sql/adb/17_agent_ords_security_local_vpd_setup.sql index b3c0f95..6fb70da 100644 --- a/sql/adb/17_agent_ords_security_local_vpd_setup.sql +++ b/sql/adb/17_agent_ords_security_local_vpd_setup.sql @@ -51,8 +51,9 @@ CREATE TABLE cb_app_user ( ); CREATE TABLE cb_app_role ( - role_id NUMBER PRIMARY KEY, - role_name VARCHAR2(50) NOT NULL + role_id NUMBER PRIMARY KEY, + role_name VARCHAR2(50) NOT NULL, + max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL ); CREATE TABLE cb_user_role ( @@ -99,9 +100,9 @@ INSERT INTO cb_app_user VALUES (101, 'agent_hr', 'E10234', 'HR', 'N', 'Y' INSERT INTO cb_app_user VALUES (102, 'agent_fin_self', 'E2001', 'FIN', 'N', 'Y'); INSERT INTO cb_app_user VALUES (103, 'agent_all', 'E99999', 'HQ', 'Y', 'Y'); -INSERT INTO cb_app_role VALUES (10, 'HR_DEPT_ROLE'); -INSERT INTO cb_app_role VALUES (20, 'FIN_SELF_ROLE'); -INSERT INTO cb_app_role VALUES (30, 'ALL_DOC_ROLE'); +INSERT INTO cb_app_role VALUES (10, 'HR_DEPT_ROLE', 'INTERNAL'); +INSERT INTO cb_app_role VALUES (20, 'FIN_SELF_ROLE', 'INTERNAL'); +INSERT INTO cb_app_role VALUES (30, 'ALL_DOC_ROLE', 'CONFIDENTIAL'); INSERT INTO cb_user_role VALUES (101, 10); INSERT INTO cb_user_role VALUES (102, 20); diff --git a/sql/adb/25_agent_ords_security_backoffice_support.sql b/sql/adb/25_agent_ords_security_backoffice_support.sql index feab13f..2d65d2c 100644 --- a/sql/adb/25_agent_ords_security_backoffice_support.sql +++ b/sql/adb/25_agent_ords_security_backoffice_support.sql @@ -62,6 +62,16 @@ EXCEPTION END; / +BEGIN + EXECUTE IMMEDIATE 'ALTER TABLE cb_app_role ADD (max_sensitivity_level VARCHAR2(20) DEFAULT ''PUBLIC'' NOT NULL)'; +EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -1430 THEN + RAISE; + END IF; +END; +/ + CREATE TABLE cb_protected_object ( object_id NUMBER PRIMARY KEY, owner VARCHAR2(128) NOT NULL, @@ -76,9 +86,38 @@ CREATE TABLE cb_protected_column ( column_name VARCHAR2(128) NOT NULL, sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL, visible_role_id NUMBER, + sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL, + redaction_method VARCHAR2(20) DEFAULT 'NONE' NOT NULL, CONSTRAINT cb_protected_column_uk UNIQUE (object_id, column_name) ); +BEGIN + EXECUTE IMMEDIATE 'ALTER TABLE cb_protected_column ADD (sensitivity_level VARCHAR2(20) DEFAULT ''PUBLIC'' NOT NULL)'; +EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -1430 THEN + RAISE; + END IF; +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'ALTER TABLE cb_protected_column ADD (redaction_method VARCHAR2(20) DEFAULT ''NONE'' NOT NULL)'; +EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -1430 THEN + RAISE; + END IF; +END; +/ + +UPDATE cb_protected_column +SET sensitivity_level = CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END, + redaction_method = CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END +WHERE sensitivity_level = 'PUBLIC' +AND redaction_method = 'NONE' +AND sensitive_yn = 'Y'; + CREATE TABLE cb_permission_column ( permission_id NUMBER NOT NULL, column_name VARCHAR2(128) NOT NULL, @@ -146,16 +185,23 @@ VALUES (src.object_id, src.owner, src.object_name, src.ords_path, src.enabled_yn MERGE INTO cb_protected_column dst USING ( - SELECT 1 column_id, 1 object_id, 'DOC_ID' column_name, 'N' sensitive_yn FROM dual UNION ALL - SELECT 2, 1, 'TITLE', 'N' FROM dual UNION ALL - SELECT 3, 1, 'OWNER_EMP_NO', 'N' FROM dual UNION ALL - SELECT 4, 1, 'DEPT_CODE', 'N' FROM dual UNION ALL - SELECT 5, 1, 'CONTENTS', 'Y' FROM dual + SELECT 1 column_id, 1 object_id, 'DOC_ID' column_name, 'N' sensitive_yn, 'PUBLIC' sensitivity_level, 'NONE' redaction_method FROM dual UNION ALL + SELECT 2, 1, 'TITLE', 'N', 'PUBLIC', 'NONE' FROM dual UNION ALL + SELECT 3, 1, 'OWNER_EMP_NO', 'N', 'INTERNAL', 'NONE' FROM dual UNION ALL + SELECT 4, 1, 'DEPT_CODE', 'N', 'INTERNAL', 'NONE' FROM dual UNION ALL + SELECT 5, 1, 'CONTENTS', 'Y', 'CONFIDENTIAL', 'NULLIFY' FROM dual ) src ON (dst.object_id = src.object_id AND dst.column_name = src.column_name) -WHEN MATCHED THEN UPDATE SET dst.sensitive_yn = src.sensitive_yn -WHEN NOT MATCHED THEN INSERT (column_id, object_id, column_name, sensitive_yn) -VALUES (src.column_id, src.object_id, src.column_name, src.sensitive_yn); +WHEN MATCHED THEN UPDATE SET + dst.sensitive_yn = src.sensitive_yn, + dst.sensitivity_level = src.sensitivity_level, + dst.redaction_method = src.redaction_method +WHEN NOT MATCHED THEN INSERT ( + column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method +) +VALUES ( + src.column_id, src.object_id, src.column_name, src.sensitive_yn, src.sensitivity_level, src.redaction_method +); MERGE INTO cb_permission_column dst USING ( diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/AppRole.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/AppRole.java index eba793a..9549b71 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/AppRole.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/AppRole.java @@ -3,6 +3,7 @@ package com.cloudhandson.vpdbackoffice.domain.permission; public record AppRole( long roleId, String roleName, - String description + String description, + String maxSensitivityLevel ) { } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/ProtectedColumn.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/ProtectedColumn.java index ef914f8..6957596 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/ProtectedColumn.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/ProtectedColumn.java @@ -5,10 +5,19 @@ public record ProtectedColumn( long objectId, String columnName, String sensitiveYn, - Long visibleRoleId + Long visibleRoleId, + String sensitivityLevel, + String redactionMethod ) { public boolean sensitive() { - return "Y".equalsIgnoreCase(sensitiveYn); + return "Y".equalsIgnoreCase(sensitiveYn) + || !"PUBLIC".equalsIgnoreCase(sensitivityLevel); + } + + public String policyLabel() { + String level = sensitivityLevel == null || sensitivityLevel.isBlank() ? "PUBLIC" : sensitivityLevel; + String method = redactionMethod == null || redactionMethod.isBlank() ? "NONE" : redactionMethod; + return level + "/" + method; } } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java index 63871c4..53f18ba 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java @@ -19,7 +19,11 @@ public interface PermissionMapper { void insertRole(@Param("roleId") long roleId, @Param("roleName") String roleName, - @Param("description") String description); + @Param("description") String description, + @Param("maxSensitivityLevel") String maxSensitivityLevel); + + int updateRoleMaxSensitivity(@Param("roleId") long roleId, + @Param("maxSensitivityLevel") String maxSensitivityLevel); int deleteRole(@Param("roleId") long roleId); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java index dd7821d..80f8510 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java @@ -34,7 +34,13 @@ public interface ProtectedObjectMapper { void insertColumn(@Param("columnId") long columnId, @Param("objectId") long objectId, @Param("columnName") String columnName, - @Param("sensitiveYn") String sensitiveYn); + @Param("sensitiveYn") String sensitiveYn, + @Param("sensitivityLevel") String sensitivityLevel, + @Param("redactionMethod") String redactionMethod); + + int updateColumnPolicy(@Param("columnId") long columnId, + @Param("sensitivityLevel") String sensitivityLevel, + @Param("redactionMethod") String redactionMethod); int updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java index fa492f2..f280d30 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java @@ -40,10 +40,13 @@ public class BackofficeSchemaService { """); createTable(results, "cb_app_role", """ CREATE TABLE cb_app_role ( - role_id NUMBER PRIMARY KEY, - role_name VARCHAR2(100) NOT NULL UNIQUE + role_id NUMBER PRIMARY KEY, + role_name VARCHAR2(100) NOT NULL UNIQUE, + max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL ) """); + addColumn(results, "cb_app_role", "max_sensitivity_level", + "ALTER TABLE cb_app_role ADD (max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL)"); createTable(results, "cb_user_role", """ CREATE TABLE cb_user_role ( user_id NUMBER NOT NULL, @@ -108,9 +111,23 @@ public class BackofficeSchemaService { column_name VARCHAR2(128) NOT NULL, sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL, visible_role_id NUMBER, + sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL, + redaction_method VARCHAR2(20) DEFAULT 'NONE' NOT NULL, CONSTRAINT cb_protected_column_uk UNIQUE (object_id, column_name) ) """); + addColumn(results, "cb_protected_column", "sensitivity_level", + "ALTER TABLE cb_protected_column ADD (sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL)"); + addColumn(results, "cb_protected_column", "redaction_method", + "ALTER TABLE cb_protected_column ADD (redaction_method VARCHAR2(20) DEFAULT 'NONE' NOT NULL)"); + jdbcTemplate.update(""" + UPDATE cb_protected_column + SET sensitivity_level = CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END, + redaction_method = CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END + WHERE sensitivity_level = 'PUBLIC' + AND redaction_method = 'NONE' + AND sensitive_yn = 'Y' + """); createTable(results, "cb_ords_probe_audit", """ CREATE TABLE cb_ords_probe_audit ( audit_id NUMBER PRIMARY KEY, diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java index a998fe8..c750588 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java @@ -192,28 +192,27 @@ public class OrdsProbeService { if (rows.isEmpty()) { return List.of(); } - List sensitiveColumns = protectedObjectService.findColumns(objectId).stream() + List sensitiveColumns = protectedObjectService.findColumns(objectId).stream() .filter(ProtectedColumn::sensitive) - .map(column -> column.columnName().toLowerCase(Locale.ROOT)) .toList(); if (sensitiveColumns.isEmpty()) { return List.of(); } List masked = new ArrayList<>(); - for (String column : sensitiveColumns) { + for (ProtectedColumn column : sensitiveColumns) { boolean present = false; boolean allNull = true; for (Map row : rows) { for (Map.Entry entry : row.entrySet()) { - if (entry.getKey().equalsIgnoreCase(column)) { + if (entry.getKey().equalsIgnoreCase(column.columnName())) { present = true; allNull = allNull && entry.getValue() == null; } } } if (present && allNull) { - masked.add(column); + masked.add(column.columnName().toLowerCase(Locale.ROOT) + " [" + column.policyLabel() + "]"); } } return masked; diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java index 9014649..dc5e592 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java @@ -23,6 +23,8 @@ public class PermissionService { private static final Set VALUE_REQUIRED_RULE_TYPES = Set.of("=", "!=", "DEPT", "EMP_NO"); private static final Set DEFAULT_COLUMN_RULE_TYPES = Set.of("MY_DEPT", "SELF", "DEPT", "EMP_NO"); private static final Set PERMISSION_EFFECTS = Set.of("ALLOW", "DENY"); + private static final Set SENSITIVITY_LEVELS = Set.of( + "PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED"); private final PermissionMapper permissionMapper; private final ProtectedObjectService protectedObjectService; @@ -48,14 +50,30 @@ public class PermissionService { @Transactional public void createRole(String roleName, String description) { + createRole(roleName, description, "PUBLIC"); + } + + @Transactional + public void createRole(String roleName, String description, String maxSensitivityLevel) { if (roleName == null || roleName.isBlank()) { throw new AppException("역할명은 필수입니다."); } long roleId = permissionMapper.nextRoleId(); - permissionMapper.insertRole(roleId, roleName.trim(), description); + permissionMapper.insertRole(roleId, roleName.trim(), description, normalizeSensitivityLevel(maxSensitivityLevel)); auditService.record(new AuditEvent("ROLE_CREATED", null, null, "SUCCESS", null, null, roleName)); } + @Transactional + public void updateRoleMaxSensitivity(long roleId, String maxSensitivityLevel) { + String normalized = normalizeSensitivityLevel(maxSensitivityLevel); + int updated = permissionMapper.updateRoleMaxSensitivity(roleId, normalized); + if (updated == 0) { + throw new AppException("수정할 역할을 찾을 수 없습니다."); + } + auditService.record(new AuditEvent("ROLE_MAX_SENSITIVITY_UPDATED", null, null, "SUCCESS", null, null, + "roleId=" + roleId + ", max=" + normalized)); + } + @Transactional public void deleteRole(long roleId) { int deleted = permissionMapper.deleteRole(roleId); @@ -196,6 +214,14 @@ public class PermissionService { return effect; } + private String normalizeSensitivityLevel(String value) { + String level = clean(value).isBlank() ? "PUBLIC" : normalize(value); + if (!SENSITIVITY_LEVELS.contains(level)) { + throw new AppException("허용되지 않은 민감도 등급입니다: " + level); + } + return level; + } + private String normalizeNullable(String value) { String cleaned = clean(value); return cleaned.isBlank() ? null : cleaned.toUpperCase(Locale.ROOT); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java index a9a7820..97548c9 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java @@ -25,6 +25,9 @@ public class ProtectedObjectService { private final Map>> databaseColumnsCache = new ConcurrentHashMap<>(); private final Map>> protectedColumnsCache = new ConcurrentHashMap<>(); private static final long CATALOG_CACHE_MILLIS = 60_000L; + private static final Set SENSITIVITY_LEVELS = Set.of( + "PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED"); + private static final Set REDACTION_METHODS = Set.of("NONE", "NULLIFY", "PARTIAL", "FULL"); public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) { this.mapper = mapper; @@ -90,7 +93,9 @@ public class ProtectedObjectService { mapper.insertObject(objectId, normalized); Set sensitive = splitCsv(normalized.sensitiveColumns()); for (String column : splitCsv(normalized.columns())) { - mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N"); + String sensitiveYn = sensitive.contains(column) ? "Y" : "N"; + mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitiveYn, + defaultSensitivityLevel(sensitiveYn), defaultRedactionMethod(sensitiveYn)); } databaseObjectsCache = null; protectedColumnsCache.remove(objectId); @@ -137,7 +142,7 @@ public class ProtectedObjectService { long objectId = mapper.nextObjectId(); mapper.insertObject(objectId, command); for (String column : columns) { - mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N"); + mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N", "PUBLIC", "NONE"); } databaseObjectsCache = null; protectedColumnsCache.remove(objectId); @@ -187,6 +192,22 @@ public class ProtectedObjectService { ordsPath.trim())); } + @Transactional + public void updateColumnPolicy(long columnId, String sensitivityLevel, String redactionMethod) { + String normalizedLevel = normalizeOption(sensitivityLevel, "PUBLIC", SENSITIVITY_LEVELS, "민감도 등급"); + String normalizedMethod = normalizeOption(redactionMethod, "NONE", REDACTION_METHODS, "마스킹 방식"); + if ("PUBLIC".equals(normalizedLevel) && !"NONE".equals(normalizedMethod)) { + throw new AppException("PUBLIC 컬럼의 마스킹 방식은 NONE이어야 합니다."); + } + int updated = mapper.updateColumnPolicy(columnId, normalizedLevel, normalizedMethod); + if (updated == 0) { + throw new AppException("수정할 컬럼 정책을 찾을 수 없습니다."); + } + protectedColumnsCache.clear(); + auditService.record(new AuditEvent("PROTECTED_COLUMN_POLICY_UPDATED", null, null, "SUCCESS", null, null, + "columnId=" + columnId + ", " + normalizedLevel + "/" + normalizedMethod)); + } + @Transactional public void disableObject(long objectId) { int updated = mapper.disableObject(objectId); @@ -211,6 +232,24 @@ public class ProtectedObjectService { return result; } + private String defaultSensitivityLevel(String sensitiveYn) { + return "Y".equalsIgnoreCase(sensitiveYn) ? "CONFIDENTIAL" : "PUBLIC"; + } + + private String defaultRedactionMethod(String sensitiveYn) { + return "Y".equalsIgnoreCase(sensitiveYn) ? "NULLIFY" : "NONE"; + } + + private String normalizeOption(String value, String defaultValue, Set allowed, String label) { + String normalized = value == null || value.isBlank() + ? defaultValue + : value.trim().toUpperCase(Locale.ROOT); + if (!allowed.contains(normalized)) { + throw new AppException("허용되지 않은 " + label + "입니다: " + normalized); + } + return normalized; + } + private record CacheEntry(T value, long expiresAt) { boolean expired() { diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java index 665681c..11d0f57 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java @@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCrea import com.cloudhandson.vpdbackoffice.service.AppException; import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService; import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService; +import java.util.stream.Collectors; import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -28,8 +29,14 @@ public class ProtectedObjectController { @GetMapping("/objects") public String objects(Model model) { - model.addAttribute("objects", protectedObjectService.findEnabled()); + var objects = protectedObjectService.findEnabled(); + model.addAttribute("objects", objects); model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects()); + model.addAttribute("columnsByObject", objects.stream() + .collect(Collectors.toMap( + object -> object.objectId(), + object -> protectedObjectService.findColumns(object.objectId()) + ))); return "objects"; } @@ -65,6 +72,22 @@ public class ProtectedObjectController { return "redirect:/objects"; } + @PostMapping("/objects/column-policy") + public String updateColumnPolicy( + @RequestParam long columnId, + @RequestParam String sensitivityLevel, + @RequestParam String redactionMethod, + RedirectAttributes redirectAttributes + ) { + try { + protectedObjectService.updateColumnPolicy(columnId, sensitivityLevel, redactionMethod); + redirectAttributes.addFlashAttribute("message", "컬럼 민감도/마스킹 정책을 수정했습니다."); + } catch (AppException exception) { + redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage()); + } + return "redirect:/objects"; + } + @PostMapping("/objects/ords-handler") public String createOrdsHandler(@RequestParam long objectId, RedirectAttributes redirectAttributes) { try { diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java index 05d7f1e..04769a8 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java @@ -27,13 +27,25 @@ public class RoleController { public String create( @RequestParam String roleName, @RequestParam(required = false) String description, + @RequestParam(defaultValue = "PUBLIC") String maxSensitivityLevel, RedirectAttributes redirectAttributes ) { - permissionService.createRole(roleName, description); + permissionService.createRole(roleName, description, maxSensitivityLevel); redirectAttributes.addFlashAttribute("message", "역할을 추가했습니다."); return "redirect:/roles"; } + @PostMapping("/roles/max-sensitivity") + public String updateMaxSensitivity( + @RequestParam long roleId, + @RequestParam String maxSensitivityLevel, + RedirectAttributes redirectAttributes + ) { + permissionService.updateRoleMaxSensitivity(roleId, maxSensitivityLevel); + redirectAttributes.addFlashAttribute("message", "역할 민감도 허용 상한을 수정했습니다."); + return "redirect:/roles"; + } + @PostMapping("/roles/delete") public String delete(@RequestParam long roleId, RedirectAttributes redirectAttributes) { permissionService.deleteRole(roleId); diff --git a/src/main/resources/mapper/PermissionMapper.xml b/src/main/resources/mapper/PermissionMapper.xml index d31eaf8..0a016d6 100644 --- a/src/main/resources/mapper/PermissionMapper.xml +++ b/src/main/resources/mapper/PermissionMapper.xml @@ -3,13 +3,19 @@ "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> @@ -19,10 +25,20 @@ - INSERT INTO cb_app_role (role_id, role_name) - VALUES (#{roleId,jdbcType=NUMERIC}, UPPER(#{roleName,jdbcType=VARCHAR})) + INSERT INTO cb_app_role (role_id, role_name, max_sensitivity_level) + VALUES ( + #{roleId,jdbcType=NUMERIC}, + UPPER(#{roleName,jdbcType=VARCHAR}), + #{maxSensitivityLevel,jdbcType=VARCHAR} + ) + + UPDATE cb_app_role + SET max_sensitivity_level = #{maxSensitivityLevel,jdbcType=VARCHAR} + WHERE role_id = #{roleId,jdbcType=NUMERIC} + + DELETE FROM cb_app_role WHERE role_id = #{roleId,jdbcType=NUMERIC} diff --git a/src/main/resources/mapper/ProtectedObjectMapper.xml b/src/main/resources/mapper/ProtectedObjectMapper.xml index 1a95e26..7c59cfe 100644 --- a/src/main/resources/mapper/ProtectedObjectMapper.xml +++ b/src/main/resources/mapper/ProtectedObjectMapper.xml @@ -57,7 +57,13 @@ + + + + + + + + + + PUBLIC/NONE + + +
+ + + + + 등록된 컬럼 정책이 없습니다. + + + + +
소스 보기를 누르면 기본 Handler PL/SQL이 표시됩니다.
diff --git a/src/main/resources/templates/roles.html b/src/main/resources/templates/roles.html index 86019e8..f0adc87 100644 --- a/src/main/resources/templates/roles.html +++ b/src/main/resources/templates/roles.html @@ -23,6 +23,15 @@ 설명 + @@ -35,6 +44,7 @@ ID 역할명 + 민감도 허용 상한 @@ -42,6 +52,19 @@ 10 HR_DEPT_ROLE + +
+ + + + +
+
@@ -51,7 +74,7 @@ - 등록된 역할이 없습니다. + 등록된 역할이 없습니다. diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java index ac195fd..b280281 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java @@ -38,8 +38,8 @@ class PermissionServiceTest { @Override public List findColumns(long objectId) { return List.of( - new ProtectedColumn(1L, 1L, "DEPT_CODE", "N", null), - new ProtectedColumn(2L, 1L, "OWNER_EMP_NO", "N", null) + new ProtectedColumn(1L, 1L, "DEPT_CODE", "N", null, "INTERNAL", "NONE"), + new ProtectedColumn(2L, 1L, "OWNER_EMP_NO", "N", null, "INTERNAL", "NONE") ); } }; @@ -184,12 +184,12 @@ class PermissionServiceTest { @Override public List findRoles() { - return List.of(new AppRole(10L, "HR_DEPT_ROLE", null)); + return List.of(new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL")); } @Override public AppRole findRole(long roleId) { - return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null) : null; + return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL") : null; } @Override @@ -198,7 +198,12 @@ class PermissionServiceTest { } @Override - public void insertRole(long roleId, String roleName, String description) { + public void insertRole(long roleId, String roleName, String description, String maxSensitivityLevel) { + } + + @Override + public int updateRoleMaxSensitivity(long roleId, String maxSensitivityLevel) { + return 1; } @Override