feat #467: separate column sensitivity policies
This commit is contained in:
35
docs/design/467-column-sensitivity-redaction/README.md
Normal file
35
docs/design/467-column-sensitivity-redaction/README.md
Normal file
@@ -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과 호환되도록 표시 예외 구조를 유지한다.
|
||||||
@@ -51,8 +51,9 @@ CREATE TABLE cb_app_user (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE cb_app_role (
|
CREATE TABLE cb_app_role (
|
||||||
role_id NUMBER PRIMARY KEY,
|
role_id NUMBER PRIMARY KEY,
|
||||||
role_name VARCHAR2(50) NOT NULL
|
role_name VARCHAR2(50) NOT NULL,
|
||||||
|
max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE cb_user_role (
|
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 (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_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 (10, 'HR_DEPT_ROLE', 'INTERNAL');
|
||||||
INSERT INTO cb_app_role VALUES (20, 'FIN_SELF_ROLE');
|
INSERT INTO cb_app_role VALUES (20, 'FIN_SELF_ROLE', 'INTERNAL');
|
||||||
INSERT INTO cb_app_role VALUES (30, 'ALL_DOC_ROLE');
|
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 (101, 10);
|
||||||
INSERT INTO cb_user_role VALUES (102, 20);
|
INSERT INTO cb_user_role VALUES (102, 20);
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ EXCEPTION
|
|||||||
END;
|
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 (
|
CREATE TABLE cb_protected_object (
|
||||||
object_id NUMBER PRIMARY KEY,
|
object_id NUMBER PRIMARY KEY,
|
||||||
owner VARCHAR2(128) NOT NULL,
|
owner VARCHAR2(128) NOT NULL,
|
||||||
@@ -76,9 +86,38 @@ CREATE TABLE cb_protected_column (
|
|||||||
column_name VARCHAR2(128) NOT NULL,
|
column_name VARCHAR2(128) NOT NULL,
|
||||||
sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL,
|
sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL,
|
||||||
visible_role_id NUMBER,
|
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)
|
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 (
|
CREATE TABLE cb_permission_column (
|
||||||
permission_id NUMBER NOT NULL,
|
permission_id NUMBER NOT NULL,
|
||||||
column_name VARCHAR2(128) 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
|
MERGE INTO cb_protected_column dst
|
||||||
USING (
|
USING (
|
||||||
SELECT 1 column_id, 1 object_id, 'DOC_ID' column_name, 'N' sensitive_yn FROM dual UNION ALL
|
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' FROM dual UNION ALL
|
SELECT 2, 1, 'TITLE', 'N', 'PUBLIC', 'NONE' FROM dual UNION ALL
|
||||||
SELECT 3, 1, 'OWNER_EMP_NO', 'N' FROM dual UNION ALL
|
SELECT 3, 1, 'OWNER_EMP_NO', 'N', 'INTERNAL', 'NONE' FROM dual UNION ALL
|
||||||
SELECT 4, 1, 'DEPT_CODE', 'N' FROM dual UNION ALL
|
SELECT 4, 1, 'DEPT_CODE', 'N', 'INTERNAL', 'NONE' FROM dual UNION ALL
|
||||||
SELECT 5, 1, 'CONTENTS', 'Y' FROM dual
|
SELECT 5, 1, 'CONTENTS', 'Y', 'CONFIDENTIAL', 'NULLIFY' FROM dual
|
||||||
) src
|
) src
|
||||||
ON (dst.object_id = src.object_id AND dst.column_name = src.column_name)
|
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 MATCHED THEN UPDATE SET
|
||||||
WHEN NOT MATCHED THEN INSERT (column_id, object_id, column_name, sensitive_yn)
|
dst.sensitive_yn = src.sensitive_yn,
|
||||||
VALUES (src.column_id, src.object_id, src.column_name, 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
|
MERGE INTO cb_permission_column dst
|
||||||
USING (
|
USING (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cloudhandson.vpdbackoffice.domain.permission;
|
|||||||
public record AppRole(
|
public record AppRole(
|
||||||
long roleId,
|
long roleId,
|
||||||
String roleName,
|
String roleName,
|
||||||
String description
|
String description,
|
||||||
|
String maxSensitivityLevel
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,19 @@ public record ProtectedColumn(
|
|||||||
long objectId,
|
long objectId,
|
||||||
String columnName,
|
String columnName,
|
||||||
String sensitiveYn,
|
String sensitiveYn,
|
||||||
Long visibleRoleId
|
Long visibleRoleId,
|
||||||
|
String sensitivityLevel,
|
||||||
|
String redactionMethod
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public boolean sensitive() {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ public interface PermissionMapper {
|
|||||||
|
|
||||||
void insertRole(@Param("roleId") long roleId,
|
void insertRole(@Param("roleId") long roleId,
|
||||||
@Param("roleName") String roleName,
|
@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);
|
int deleteRole(@Param("roleId") long roleId);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ public interface ProtectedObjectMapper {
|
|||||||
void insertColumn(@Param("columnId") long columnId,
|
void insertColumn(@Param("columnId") long columnId,
|
||||||
@Param("objectId") long objectId,
|
@Param("objectId") long objectId,
|
||||||
@Param("columnName") String columnName,
|
@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);
|
int updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath);
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,13 @@ public class BackofficeSchemaService {
|
|||||||
""");
|
""");
|
||||||
createTable(results, "cb_app_role", """
|
createTable(results, "cb_app_role", """
|
||||||
CREATE TABLE cb_app_role (
|
CREATE TABLE cb_app_role (
|
||||||
role_id NUMBER PRIMARY KEY,
|
role_id NUMBER PRIMARY KEY,
|
||||||
role_name VARCHAR2(100) NOT NULL UNIQUE
|
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", """
|
createTable(results, "cb_user_role", """
|
||||||
CREATE TABLE cb_user_role (
|
CREATE TABLE cb_user_role (
|
||||||
user_id NUMBER NOT NULL,
|
user_id NUMBER NOT NULL,
|
||||||
@@ -108,9 +111,23 @@ public class BackofficeSchemaService {
|
|||||||
column_name VARCHAR2(128) NOT NULL,
|
column_name VARCHAR2(128) NOT NULL,
|
||||||
sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL,
|
sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL,
|
||||||
visible_role_id NUMBER,
|
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)
|
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", """
|
createTable(results, "cb_ords_probe_audit", """
|
||||||
CREATE TABLE cb_ords_probe_audit (
|
CREATE TABLE cb_ords_probe_audit (
|
||||||
audit_id NUMBER PRIMARY KEY,
|
audit_id NUMBER PRIMARY KEY,
|
||||||
|
|||||||
@@ -192,28 +192,27 @@ public class OrdsProbeService {
|
|||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<String> sensitiveColumns = protectedObjectService.findColumns(objectId).stream()
|
List<ProtectedColumn> sensitiveColumns = protectedObjectService.findColumns(objectId).stream()
|
||||||
.filter(ProtectedColumn::sensitive)
|
.filter(ProtectedColumn::sensitive)
|
||||||
.map(column -> column.columnName().toLowerCase(Locale.ROOT))
|
|
||||||
.toList();
|
.toList();
|
||||||
if (sensitiveColumns.isEmpty()) {
|
if (sensitiveColumns.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> masked = new ArrayList<>();
|
List<String> masked = new ArrayList<>();
|
||||||
for (String column : sensitiveColumns) {
|
for (ProtectedColumn column : sensitiveColumns) {
|
||||||
boolean present = false;
|
boolean present = false;
|
||||||
boolean allNull = true;
|
boolean allNull = true;
|
||||||
for (Map<String, Object> row : rows) {
|
for (Map<String, Object> row : rows) {
|
||||||
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
||||||
if (entry.getKey().equalsIgnoreCase(column)) {
|
if (entry.getKey().equalsIgnoreCase(column.columnName())) {
|
||||||
present = true;
|
present = true;
|
||||||
allNull = allNull && entry.getValue() == null;
|
allNull = allNull && entry.getValue() == null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (present && allNull) {
|
if (present && allNull) {
|
||||||
masked.add(column);
|
masked.add(column.columnName().toLowerCase(Locale.ROOT) + " [" + column.policyLabel() + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return masked;
|
return masked;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class PermissionService {
|
|||||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of("=", "!=", "DEPT", "EMP_NO");
|
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of("=", "!=", "DEPT", "EMP_NO");
|
||||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of("MY_DEPT", "SELF", "DEPT", "EMP_NO");
|
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of("MY_DEPT", "SELF", "DEPT", "EMP_NO");
|
||||||
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
||||||
|
private static final Set<String> SENSITIVITY_LEVELS = Set.of(
|
||||||
|
"PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED");
|
||||||
|
|
||||||
private final PermissionMapper permissionMapper;
|
private final PermissionMapper permissionMapper;
|
||||||
private final ProtectedObjectService protectedObjectService;
|
private final ProtectedObjectService protectedObjectService;
|
||||||
@@ -48,14 +50,30 @@ public class PermissionService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void createRole(String roleName, String description) {
|
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()) {
|
if (roleName == null || roleName.isBlank()) {
|
||||||
throw new AppException("역할명은 필수입니다.");
|
throw new AppException("역할명은 필수입니다.");
|
||||||
}
|
}
|
||||||
long roleId = permissionMapper.nextRoleId();
|
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));
|
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
|
@Transactional
|
||||||
public void deleteRole(long roleId) {
|
public void deleteRole(long roleId) {
|
||||||
int deleted = permissionMapper.deleteRole(roleId);
|
int deleted = permissionMapper.deleteRole(roleId);
|
||||||
@@ -196,6 +214,14 @@ public class PermissionService {
|
|||||||
return effect;
|
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) {
|
private String normalizeNullable(String value) {
|
||||||
String cleaned = clean(value);
|
String cleaned = clean(value);
|
||||||
return cleaned.isBlank() ? null : cleaned.toUpperCase(Locale.ROOT);
|
return cleaned.isBlank() ? null : cleaned.toUpperCase(Locale.ROOT);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public class ProtectedObjectService {
|
|||||||
private final Map<String, CacheEntry<List<String>>> databaseColumnsCache = new ConcurrentHashMap<>();
|
private final Map<String, CacheEntry<List<String>>> databaseColumnsCache = new ConcurrentHashMap<>();
|
||||||
private final Map<Long, CacheEntry<List<ProtectedColumn>>> protectedColumnsCache = new ConcurrentHashMap<>();
|
private final Map<Long, CacheEntry<List<ProtectedColumn>>> protectedColumnsCache = new ConcurrentHashMap<>();
|
||||||
private static final long CATALOG_CACHE_MILLIS = 60_000L;
|
private static final long CATALOG_CACHE_MILLIS = 60_000L;
|
||||||
|
private static final Set<String> SENSITIVITY_LEVELS = Set.of(
|
||||||
|
"PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED");
|
||||||
|
private static final Set<String> REDACTION_METHODS = Set.of("NONE", "NULLIFY", "PARTIAL", "FULL");
|
||||||
|
|
||||||
public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
|
public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
@@ -90,7 +93,9 @@ public class ProtectedObjectService {
|
|||||||
mapper.insertObject(objectId, normalized);
|
mapper.insertObject(objectId, normalized);
|
||||||
Set<String> sensitive = splitCsv(normalized.sensitiveColumns());
|
Set<String> sensitive = splitCsv(normalized.sensitiveColumns());
|
||||||
for (String column : splitCsv(normalized.columns())) {
|
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;
|
databaseObjectsCache = null;
|
||||||
protectedColumnsCache.remove(objectId);
|
protectedColumnsCache.remove(objectId);
|
||||||
@@ -137,7 +142,7 @@ public class ProtectedObjectService {
|
|||||||
long objectId = mapper.nextObjectId();
|
long objectId = mapper.nextObjectId();
|
||||||
mapper.insertObject(objectId, command);
|
mapper.insertObject(objectId, command);
|
||||||
for (String column : columns) {
|
for (String column : columns) {
|
||||||
mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N");
|
mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N", "PUBLIC", "NONE");
|
||||||
}
|
}
|
||||||
databaseObjectsCache = null;
|
databaseObjectsCache = null;
|
||||||
protectedColumnsCache.remove(objectId);
|
protectedColumnsCache.remove(objectId);
|
||||||
@@ -187,6 +192,22 @@ public class ProtectedObjectService {
|
|||||||
ordsPath.trim()));
|
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
|
@Transactional
|
||||||
public void disableObject(long objectId) {
|
public void disableObject(long objectId) {
|
||||||
int updated = mapper.disableObject(objectId);
|
int updated = mapper.disableObject(objectId);
|
||||||
@@ -211,6 +232,24 @@ public class ProtectedObjectService {
|
|||||||
return result;
|
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<String> 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>(T value, long expiresAt) {
|
private record CacheEntry<T>(T value, long expiresAt) {
|
||||||
|
|
||||||
boolean expired() {
|
boolean expired() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCrea
|
|||||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||||
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
|
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
|
||||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@@ -28,8 +29,14 @@ public class ProtectedObjectController {
|
|||||||
|
|
||||||
@GetMapping("/objects")
|
@GetMapping("/objects")
|
||||||
public String objects(Model model) {
|
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("dbObjects", protectedObjectService.findDatabaseObjects());
|
||||||
|
model.addAttribute("columnsByObject", objects.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
object -> object.objectId(),
|
||||||
|
object -> protectedObjectService.findColumns(object.objectId())
|
||||||
|
)));
|
||||||
return "objects";
|
return "objects";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +72,22 @@ public class ProtectedObjectController {
|
|||||||
return "redirect:/objects";
|
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")
|
@PostMapping("/objects/ords-handler")
|
||||||
public String createOrdsHandler(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
public String createOrdsHandler(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -27,13 +27,25 @@ public class RoleController {
|
|||||||
public String create(
|
public String create(
|
||||||
@RequestParam String roleName,
|
@RequestParam String roleName,
|
||||||
@RequestParam(required = false) String description,
|
@RequestParam(required = false) String description,
|
||||||
|
@RequestParam(defaultValue = "PUBLIC") String maxSensitivityLevel,
|
||||||
RedirectAttributes redirectAttributes
|
RedirectAttributes redirectAttributes
|
||||||
) {
|
) {
|
||||||
permissionService.createRole(roleName, description);
|
permissionService.createRole(roleName, description, maxSensitivityLevel);
|
||||||
redirectAttributes.addFlashAttribute("message", "역할을 추가했습니다.");
|
redirectAttributes.addFlashAttribute("message", "역할을 추가했습니다.");
|
||||||
return "redirect:/roles";
|
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")
|
@PostMapping("/roles/delete")
|
||||||
public String delete(@RequestParam long roleId, RedirectAttributes redirectAttributes) {
|
public String delete(@RequestParam long roleId, RedirectAttributes redirectAttributes) {
|
||||||
permissionService.deleteRole(roleId);
|
permissionService.deleteRole(roleId);
|
||||||
|
|||||||
@@ -3,13 +3,19 @@
|
|||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.PermissionMapper">
|
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.PermissionMapper">
|
||||||
<select id="findRoles" resultType="com.cloudhandson.vpdbackoffice.domain.permission.AppRole">
|
<select id="findRoles" resultType="com.cloudhandson.vpdbackoffice.domain.permission.AppRole">
|
||||||
SELECT role_id, role_name, CAST(NULL AS VARCHAR2(200)) AS description
|
SELECT role_id,
|
||||||
|
role_name,
|
||||||
|
CAST(NULL AS VARCHAR2(200)) AS description,
|
||||||
|
NVL(max_sensitivity_level, 'PUBLIC') AS max_sensitivity_level
|
||||||
FROM cb_app_role
|
FROM cb_app_role
|
||||||
ORDER BY role_name
|
ORDER BY role_name
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="findRole" resultType="com.cloudhandson.vpdbackoffice.domain.permission.AppRole">
|
<select id="findRole" resultType="com.cloudhandson.vpdbackoffice.domain.permission.AppRole">
|
||||||
SELECT role_id, role_name, CAST(NULL AS VARCHAR2(200)) AS description
|
SELECT role_id,
|
||||||
|
role_name,
|
||||||
|
CAST(NULL AS VARCHAR2(200)) AS description,
|
||||||
|
NVL(max_sensitivity_level, 'PUBLIC') AS max_sensitivity_level
|
||||||
FROM cb_app_role
|
FROM cb_app_role
|
||||||
WHERE role_id = #{roleId}
|
WHERE role_id = #{roleId}
|
||||||
</select>
|
</select>
|
||||||
@@ -19,10 +25,20 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insertRole">
|
<insert id="insertRole">
|
||||||
INSERT INTO cb_app_role (role_id, role_name)
|
INSERT INTO cb_app_role (role_id, role_name, max_sensitivity_level)
|
||||||
VALUES (#{roleId,jdbcType=NUMERIC}, UPPER(#{roleName,jdbcType=VARCHAR}))
|
VALUES (
|
||||||
|
#{roleId,jdbcType=NUMERIC},
|
||||||
|
UPPER(#{roleName,jdbcType=VARCHAR}),
|
||||||
|
#{maxSensitivityLevel,jdbcType=VARCHAR}
|
||||||
|
)
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateRoleMaxSensitivity">
|
||||||
|
UPDATE cb_app_role
|
||||||
|
SET max_sensitivity_level = #{maxSensitivityLevel,jdbcType=VARCHAR}
|
||||||
|
WHERE role_id = #{roleId,jdbcType=NUMERIC}
|
||||||
|
</update>
|
||||||
|
|
||||||
<delete id="deleteRole">
|
<delete id="deleteRole">
|
||||||
DELETE FROM cb_app_role
|
DELETE FROM cb_app_role
|
||||||
WHERE role_id = #{roleId,jdbcType=NUMERIC}
|
WHERE role_id = #{roleId,jdbcType=NUMERIC}
|
||||||
|
|||||||
@@ -57,7 +57,13 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="findColumns" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn">
|
<select id="findColumns" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn">
|
||||||
SELECT column_id, object_id, column_name, sensitive_yn, visible_role_id
|
SELECT column_id,
|
||||||
|
object_id,
|
||||||
|
column_name,
|
||||||
|
sensitive_yn,
|
||||||
|
visible_role_id,
|
||||||
|
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||||
|
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||||
FROM cb_protected_column
|
FROM cb_protected_column
|
||||||
WHERE object_id = #{objectId}
|
WHERE object_id = #{objectId}
|
||||||
ORDER BY column_id
|
ORDER BY column_id
|
||||||
@@ -83,10 +89,26 @@
|
|||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<insert id="insertColumn">
|
<insert id="insertColumn">
|
||||||
INSERT INTO cb_protected_column (column_id, object_id, column_name, sensitive_yn)
|
INSERT INTO cb_protected_column (
|
||||||
VALUES (#{columnId}, #{objectId}, UPPER(#{columnName}), #{sensitiveYn})
|
column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
#{columnId}, #{objectId}, UPPER(#{columnName}), #{sensitiveYn}, #{sensitivityLevel}, #{redactionMethod}
|
||||||
|
)
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateColumnPolicy">
|
||||||
|
UPDATE cb_protected_column
|
||||||
|
SET sensitivity_level = #{sensitivityLevel,jdbcType=VARCHAR},
|
||||||
|
redaction_method = #{redactionMethod,jdbcType=VARCHAR},
|
||||||
|
sensitive_yn = CASE
|
||||||
|
WHEN #{sensitivityLevel,jdbcType=VARCHAR} = 'PUBLIC'
|
||||||
|
AND #{redactionMethod,jdbcType=VARCHAR} = 'NONE' THEN 'N'
|
||||||
|
ELSE 'Y'
|
||||||
|
END
|
||||||
|
WHERE column_id = #{columnId,jdbcType=NUMERIC}
|
||||||
|
</update>
|
||||||
|
|
||||||
<update id="updateOrdsPath">
|
<update id="updateOrdsPath">
|
||||||
UPDATE cb_protected_object
|
UPDATE cb_protected_object
|
||||||
SET ords_path = #{ordsPath,jdbcType=VARCHAR}
|
SET ords_path = #{ordsPath,jdbcType=VARCHAR}
|
||||||
|
|||||||
@@ -86,6 +86,65 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="policy-source-cell">
|
<td colspan="5" class="policy-source-cell">
|
||||||
|
<details class="column-policy-panel">
|
||||||
|
<summary>컬럼 민감도/마스킹 정책</summary>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Column</th>
|
||||||
|
<th>민감도</th>
|
||||||
|
<th>마스킹</th>
|
||||||
|
<th>상태</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="column : ${columnsByObject[object.objectId()]}">
|
||||||
|
<td>
|
||||||
|
<span th:text="${column.columnName()}">CONTENTS</span>
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"
|
||||||
|
th:form="${'column-policy-form-' + column.columnId()}">
|
||||||
|
<input type="hidden" name="columnId" th:value="${column.columnId()}"
|
||||||
|
th:form="${'column-policy-form-' + column.columnId()}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-select form-select-sm" name="sensitivityLevel"
|
||||||
|
th:form="${'column-policy-form-' + column.columnId()}">
|
||||||
|
<option value="PUBLIC" th:selected="${column.sensitivityLevel() == 'PUBLIC'}">PUBLIC</option>
|
||||||
|
<option value="INTERNAL" th:selected="${column.sensitivityLevel() == 'INTERNAL'}">INTERNAL</option>
|
||||||
|
<option value="CONFIDENTIAL" th:selected="${column.sensitivityLevel() == 'CONFIDENTIAL'}">CONFIDENTIAL</option>
|
||||||
|
<option value="RESTRICTED" th:selected="${column.sensitivityLevel() == 'RESTRICTED'}">RESTRICTED</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-select form-select-sm" name="redactionMethod"
|
||||||
|
th:form="${'column-policy-form-' + column.columnId()}">
|
||||||
|
<option value="NONE" th:selected="${column.redactionMethod() == 'NONE'}">NONE</option>
|
||||||
|
<option value="NULLIFY" th:selected="${column.redactionMethod() == 'NULLIFY'}">NULLIFY</option>
|
||||||
|
<option value="PARTIAL" th:selected="${column.redactionMethod() == 'PARTIAL'}">PARTIAL</option>
|
||||||
|
<option value="FULL" th:selected="${column.redactionMethod() == 'FULL'}">FULL</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge"
|
||||||
|
th:classappend="${column.sensitive()} ? ' text-bg-warning' : ' text-bg-secondary'"
|
||||||
|
th:text="${column.policyLabel()}">PUBLIC/NONE</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/objects/column-policy"
|
||||||
|
th:id="${'column-policy-form-' + column.columnId()}"></form>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" type="submit"
|
||||||
|
th:form="${'column-policy-form-' + column.columnId()}">저장</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr th:if="${#lists.isEmpty(columnsByObject[object.objectId()])}">
|
||||||
|
<td colspan="5" class="text-muted">등록된 컬럼 정책이 없습니다.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<div th:id="${'handler-source-' + object.objectId()}" class="text-muted small">
|
<div th:id="${'handler-source-' + object.objectId()}" class="text-muted small">
|
||||||
소스 보기를 누르면 기본 Handler PL/SQL이 표시됩니다.
|
소스 보기를 누르면 기본 Handler PL/SQL이 표시됩니다.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,15 @@
|
|||||||
설명
|
설명
|
||||||
<input class="form-control" name="description" maxlength="200">
|
<input class="form-control" name="description" maxlength="200">
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
민감도 허용 상한
|
||||||
|
<select class="form-select" name="maxSensitivityLevel">
|
||||||
|
<option value="PUBLIC">PUBLIC</option>
|
||||||
|
<option value="INTERNAL">INTERNAL</option>
|
||||||
|
<option value="CONFIDENTIAL">CONFIDENTIAL</option>
|
||||||
|
<option value="RESTRICTED">RESTRICTED</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<button class="btn rw-btn-primary" type="submit">추가</button>
|
<button class="btn rw-btn-primary" type="submit">추가</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -35,6 +44,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>역할명</th>
|
<th>역할명</th>
|
||||||
|
<th>민감도 허용 상한</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -42,6 +52,19 @@
|
|||||||
<tr th:each="role : ${roles}">
|
<tr th:each="role : ${roles}">
|
||||||
<td th:text="${role.roleId()}">10</td>
|
<td th:text="${role.roleId()}">10</td>
|
||||||
<td th:text="${role.roleName()}">HR_DEPT_ROLE</td>
|
<td th:text="${role.roleName()}">HR_DEPT_ROLE</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/roles/max-sensitivity" class="inline-form">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<input type="hidden" name="roleId" th:value="${role.roleId()}">
|
||||||
|
<select class="form-select form-select-sm" name="maxSensitivityLevel">
|
||||||
|
<option value="PUBLIC" th:selected="${role.maxSensitivityLevel() == 'PUBLIC'}">PUBLIC</option>
|
||||||
|
<option value="INTERNAL" th:selected="${role.maxSensitivityLevel() == 'INTERNAL'}">INTERNAL</option>
|
||||||
|
<option value="CONFIDENTIAL" th:selected="${role.maxSensitivityLevel() == 'CONFIDENTIAL'}">CONFIDENTIAL</option>
|
||||||
|
<option value="RESTRICTED" th:selected="${role.maxSensitivityLevel() == 'RESTRICTED'}">RESTRICTED</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" type="submit">저장</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="/roles/delete" class="inline-form">
|
<form method="post" action="/roles/delete" class="inline-form">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
@@ -51,7 +74,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${#lists.isEmpty(roles)}">
|
<tr th:if="${#lists.isEmpty(roles)}">
|
||||||
<td colspan="3" class="text-muted">등록된 역할이 없습니다.</td>
|
<td colspan="4" class="text-muted">등록된 역할이 없습니다.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ class PermissionServiceTest {
|
|||||||
@Override
|
@Override
|
||||||
public List<ProtectedColumn> findColumns(long objectId) {
|
public List<ProtectedColumn> findColumns(long objectId) {
|
||||||
return List.of(
|
return List.of(
|
||||||
new ProtectedColumn(1L, 1L, "DEPT_CODE", "N", null),
|
new ProtectedColumn(1L, 1L, "DEPT_CODE", "N", null, "INTERNAL", "NONE"),
|
||||||
new ProtectedColumn(2L, 1L, "OWNER_EMP_NO", "N", null)
|
new ProtectedColumn(2L, 1L, "OWNER_EMP_NO", "N", null, "INTERNAL", "NONE")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -184,12 +184,12 @@ class PermissionServiceTest {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AppRole> findRoles() {
|
public List<AppRole> findRoles() {
|
||||||
return List.of(new AppRole(10L, "HR_DEPT_ROLE", null));
|
return List.of(new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AppRole findRole(long roleId) {
|
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
|
@Override
|
||||||
@@ -198,7 +198,12 @@ class PermissionServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user