[Developer] #619 focus POC on KB VPD permissions
This commit is contained in:
@@ -13,6 +13,8 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -22,11 +24,21 @@ public class PermissionService {
|
||||
|
||||
private static final Set<String> RULE_TYPES = Set.of(
|
||||
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
|
||||
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL",
|
||||
"TOKEN_SUBJECT", "OWN_CONTRACT", "CHANNEL_CONTRACT", "OWN_CUSTOMER", "CHANNEL_CUSTOMER",
|
||||
"STATIC_SQL");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG", "STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG", "STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL", "STATIC_SQL");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
|
||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG");
|
||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
|
||||
private static final Set<String> STATIC_SQL_KEYWORDS = Set.of(
|
||||
"AND", "OR", "NOT", "NULL", "IS", "IN", "LIKE", "BETWEEN", "ESCAPE", "TRUE", "FALSE",
|
||||
"UPPER", "LOWER", "TRIM", "NVL", "COALESCE", "TO_CHAR", "TO_DATE", "REGEXP_LIKE", "INSTR",
|
||||
"LENGTH", "SUBSTR", "REPLACE", "CAST", "AS", "DATE", "TIMESTAMP", "NUMBER", "VARCHAR2", "CHAR");
|
||||
private static final Pattern SQL_STRING_LITERAL = Pattern.compile("'(?:''|[^'])*'");
|
||||
private static final Pattern SQL_IDENTIFIER = Pattern.compile("[A-Z_$#][A-Z0-9_$#]*");
|
||||
private static final Pattern SQL_QUALIFIED_IDENTIFIER =
|
||||
Pattern.compile("[A-Z_$#][A-Z0-9_$#]*\\s*\\.", Pattern.CASE_INSENSITIVE);
|
||||
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
||||
private static final Set<String> SENSITIVITY_LEVELS = Set.of(
|
||||
"PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED");
|
||||
@@ -225,12 +237,18 @@ public class PermissionService {
|
||||
if ("TAG".equals(type) && column == null && !allowedColumns.contains("TECH_TAG")) {
|
||||
throw new AppException("TAG 규칙의 기본 컬럼 TECH_TAG가 보호 객체에 없습니다. 컬럼을 지정하세요.");
|
||||
}
|
||||
if ("STATIC_SQL".equals(type) && column != null) {
|
||||
throw new AppException("정적 SQL 조건은 컬럼 선택 없이 조건식 전체를 입력하세요.");
|
||||
}
|
||||
if (VALUE_REQUIRED_RULE_TYPES.contains(type) && ruleValue.isBlank()) {
|
||||
throw new AppException(type + " 규칙에는 값이 필요합니다.");
|
||||
}
|
||||
if ("TAG".equals(type) && !ruleValue.matches("[A-Z0-9_-]+")) {
|
||||
throw new AppException("TAG 값은 영문 대문자, 숫자, '_' 또는 '-'만 사용할 수 있습니다.");
|
||||
}
|
||||
if ("STATIC_SQL".equals(type)) {
|
||||
validateStaticSqlPredicate(ruleValue, allowedColumns);
|
||||
}
|
||||
}
|
||||
if (hasAll && rules.size() > 1) {
|
||||
throw new AppException("ALL 규칙은 다른 규칙과 함께 저장할 수 없습니다.");
|
||||
@@ -245,6 +263,41 @@ public class PermissionService {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
private void validateStaticSqlPredicate(String predicate, Set<String> allowedColumns) {
|
||||
if (predicate.length() > 1000) {
|
||||
throw new AppException("정적 SQL 조건은 1,000자 이하여야 합니다.");
|
||||
}
|
||||
if (predicate.chars().filter(value -> value == '\'').count() % 2 != 0) {
|
||||
throw new AppException("정적 SQL 조건의 문자열 따옴표가 닫히지 않았습니다.");
|
||||
}
|
||||
if (predicate.indexOf(';') >= 0 || predicate.contains("--") || predicate.contains("/*")
|
||||
|| predicate.contains("*/") || predicate.indexOf(':') >= 0 || predicate.indexOf('@') >= 0
|
||||
|| predicate.contains("||") || predicate.chars().anyMatch(Character::isISOControl)) {
|
||||
throw new AppException("정적 SQL 조건에는 단일 WHERE 조건식만 입력할 수 있습니다.");
|
||||
}
|
||||
|
||||
String lexical = SQL_STRING_LITERAL.matcher(predicate).replaceAll(" ");
|
||||
if (SQL_QUALIFIED_IDENTIFIER.matcher(lexical).find()) {
|
||||
throw new AppException("정적 SQL 조건에는 다른 테이블·스키마를 참조할 수 없습니다.");
|
||||
}
|
||||
|
||||
boolean hasObjectColumn = false;
|
||||
Matcher matcher = SQL_IDENTIFIER.matcher(lexical.toUpperCase(Locale.ROOT));
|
||||
while (matcher.find()) {
|
||||
String token = matcher.group();
|
||||
if (STATIC_SQL_KEYWORDS.contains(token)) {
|
||||
continue;
|
||||
}
|
||||
if (!allowedColumns.contains(token)) {
|
||||
throw new AppException("정적 SQL 조건에는 보호 객체 컬럼과 허용된 SQL 연산자만 사용할 수 있습니다: " + token);
|
||||
}
|
||||
hasObjectColumn = true;
|
||||
}
|
||||
if (!hasObjectColumn) {
|
||||
throw new AppException("정적 SQL 조건에는 보호 객체 컬럼이 하나 이상 필요합니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVisibleColumns(long objectId, List<String> visibleColumns) {
|
||||
if (visibleColumns == null || visibleColumns.isEmpty()) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user