refs #742: merge HMM application layout into main
# Conflicts: # src/main/java/com/cloudhandson/vpdbackoffice/service/StructuredDataService.java # vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataService.java # vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/service/VectorKnowledgeService.java # vpd-backoffice/src/main/resources/mapper/MaskingRuleMapper.xml # vpd-backoffice/src/main/resources/templates/schema-metadata.html # vpd-backoffice/src/main/resources/templates/structured-data.html
This commit is contained in:
112
vpd-backoffice/pom.xml
Normal file
112
vpd-backoffice/pom.xml
Normal file
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.cloudhandson</groupId>
|
||||
<artifactId>vpd-permission-backoffice</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>vpd-permission-backoffice</name>
|
||||
<description>Spring Boot backoffice for VPD, bearer token, and ORDS permission probes</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.8</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<mybatis-spring-boot.version>3.0.4</mybatis-spring-boot.version>
|
||||
<oracle.jdbc.version>23.6.0.24.10</oracle.jdbc.version>
|
||||
<!-- GPT-5.5 response/schema support is supplied by the current OCI SDK. -->
|
||||
<oci.sdk.version>3.90.1</oci.sdk.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${mybatis-spring-boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.jdbc</groupId>
|
||||
<artifactId>ojdbc11</artifactId>
|
||||
<version>${oracle.jdbc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.oracle.database.security</groupId>
|
||||
<artifactId>oraclepki</artifactId>
|
||||
<version>${oracle.jdbc.version}</version>
|
||||
</dependency>
|
||||
<!-- OCI API-key signing for the SQL-script explanation assistant. The
|
||||
private key remains in the configured OCI config file, never in the
|
||||
application environment or Git. -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.oci.sdk</groupId>
|
||||
<artifactId>oci-java-sdk-generativeaiinference</artifactId>
|
||||
<version>${oci.sdk.version}</version>
|
||||
</dependency>
|
||||
<!-- OCI SDK service modules need exactly one concrete HTTP transport. The
|
||||
Jersey 3 transport is compatible with this Spring Boot 3 application. -->
|
||||
<dependency>
|
||||
<groupId>com.oracle.oci.sdk</groupId>
|
||||
<artifactId>oci-java-sdk-common-httpclient-jersey3</artifactId>
|
||||
<version>${oci.sdk.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
</resource>
|
||||
<!-- Keep reviewed SQL in database/adb as the sole Git source. Runtime
|
||||
BACKOFFICE_SECURITY_SQL_SCRIPTS remains the display allow-list. -->
|
||||
<resource>
|
||||
<directory>../database/adb</directory>
|
||||
<targetPath>database/adb</targetPath>
|
||||
<includes>
|
||||
<include>**/*.sql</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cloudhandson.vpdbackoffice;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@MapperScan("com.cloudhandson.vpdbackoffice.mapper")
|
||||
@SpringBootApplication
|
||||
public class VpdBackofficeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(VpdBackofficeApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import java.time.Clock;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({
|
||||
BackofficeProperties.class,
|
||||
CatalogProperties.class,
|
||||
MaskingProperties.class,
|
||||
McpProperties.class,
|
||||
ProductProperties.class,
|
||||
SecuritySqlScriptProperties.class
|
||||
})
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
Clock clock() {
|
||||
return Clock.systemUTC();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "backoffice")
|
||||
public record BackofficeProperties(
|
||||
Security security,
|
||||
Token token,
|
||||
Ords ords,
|
||||
Ai ai,
|
||||
SelectAi selectAi
|
||||
) {
|
||||
|
||||
public record Security(
|
||||
String adminUser,
|
||||
String adminPassword,
|
||||
String adminPasswordHash,
|
||||
boolean guestEnabled,
|
||||
String guestUser,
|
||||
String guestPassword,
|
||||
String guestPasswordHash,
|
||||
boolean requireHttps,
|
||||
boolean rememberMeEnabled,
|
||||
String rememberMeKey,
|
||||
int rememberMeDays
|
||||
) {
|
||||
|
||||
/** Remember-me is intentionally unavailable over HTTP or without a stable secret key. */
|
||||
public boolean rememberMeConfigured() {
|
||||
return requireHttps
|
||||
&& rememberMeEnabled
|
||||
&& rememberMeKey != null
|
||||
&& !rememberMeKey.isBlank()
|
||||
&& rememberMeDays >= 1
|
||||
&& rememberMeDays <= 90;
|
||||
}
|
||||
|
||||
public int rememberMeValiditySeconds() {
|
||||
return rememberMeDays * 24 * 60 * 60;
|
||||
}
|
||||
|
||||
public boolean guestConfigured() {
|
||||
return guestEnabled
|
||||
&& guestUser != null
|
||||
&& !guestUser.isBlank()
|
||||
&& ((guestPassword != null && !guestPassword.isBlank())
|
||||
|| (guestPasswordHash != null && !guestPasswordHash.isBlank()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Token(int maxDays) {
|
||||
}
|
||||
|
||||
public record Ords(String baseUrl, Duration timeout, Duration agentTimeout) {
|
||||
}
|
||||
|
||||
public record Ai(
|
||||
boolean enabled,
|
||||
String provider,
|
||||
String baseUrl,
|
||||
String model,
|
||||
String apiKey,
|
||||
Duration timeout,
|
||||
String embeddingModel,
|
||||
String ociConfigFile,
|
||||
String ociProfile,
|
||||
String ociRegion,
|
||||
String ociCompartmentId
|
||||
) {
|
||||
|
||||
public Ai(boolean enabled, String baseUrl, String model, String apiKey, Duration timeout) {
|
||||
this(enabled, "openai", baseUrl, model, apiKey, timeout, "", "", "", "", "");
|
||||
}
|
||||
}
|
||||
|
||||
/** Separate ADB connection because Select AI profiles are owned by a schema-specific account. */
|
||||
public record SelectAi(
|
||||
String dbUrl,
|
||||
String dbUsername,
|
||||
String dbPassword,
|
||||
String profile,
|
||||
String runtimeDbUrl,
|
||||
String runtimeDbUsername,
|
||||
String runtimeDbPassword,
|
||||
String queryContractFile
|
||||
) {
|
||||
|
||||
public boolean configured() {
|
||||
return dbUrl != null && !dbUrl.isBlank()
|
||||
&& dbUsername != null && !dbUsername.isBlank()
|
||||
&& dbPassword != null && !dbPassword.isBlank()
|
||||
&& profile != null && !profile.isBlank();
|
||||
}
|
||||
|
||||
/** The generated SQL must never fall back to the privileged profile-owner connection. */
|
||||
public boolean runtimeConfigured() {
|
||||
return runtimeDbUrl != null && !runtimeDbUrl.isBlank()
|
||||
&& runtimeDbUsername != null && !runtimeDbUsername.isBlank()
|
||||
&& runtimeDbPassword != null && !runtimeDbPassword.isBlank();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** Deployment-provided allow-list for the structured-data and metadata screens. */
|
||||
@ConfigurationProperties(prefix = "backoffice.catalog")
|
||||
public record CatalogProperties(String owner, String objects) {
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import com.cloudhandson.vpdbackoffice.service.GroupService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
import java.sql.Connection;
|
||||
import javax.sql.DataSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DbPoolWarmup {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DbPoolWarmup.class);
|
||||
private final DataSource dataSource;
|
||||
private final UserService userService;
|
||||
private final GroupService groupService;
|
||||
private final PermissionService permissionService;
|
||||
|
||||
public DbPoolWarmup(
|
||||
DataSource dataSource,
|
||||
UserService userService,
|
||||
GroupService groupService,
|
||||
PermissionService permissionService
|
||||
) {
|
||||
this.dataSource = dataSource;
|
||||
this.userService = userService;
|
||||
this.groupService = groupService;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void warmup() {
|
||||
try (Connection ignored = dataSource.getConnection()) {
|
||||
log.info("Backoffice DB pool warmed up");
|
||||
warmupBackofficeCatalog();
|
||||
} catch (Exception exception) {
|
||||
log.warn("Backoffice DB pool warm-up failed: {}", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void warmupBackofficeCatalog() {
|
||||
long started = System.nanoTime();
|
||||
userService.findAll();
|
||||
userService.findUserRoles();
|
||||
groupService.findAll();
|
||||
groupService.findGroupUsers();
|
||||
groupService.findGroupRoles();
|
||||
permissionService.findRoles();
|
||||
permissionService.findPermissionViews();
|
||||
log.info("HMM identity catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** JSON configuration of database redaction policies this backoffice may manage. */
|
||||
@ConfigurationProperties(prefix = "backoffice.masking")
|
||||
public record MaskingProperties(String policies) {
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** Product-neutral MCP endpoint labels and tool catalogue configuration. */
|
||||
@ConfigurationProperties(prefix = "backoffice.mcp")
|
||||
public record McpProperties(
|
||||
String publicUrl,
|
||||
String serverName,
|
||||
String toolName,
|
||||
String toolLabel,
|
||||
String toolDescription,
|
||||
String promptDescription,
|
||||
String tools
|
||||
) {
|
||||
|
||||
private static final String DEFAULT_PUBLIC_URL = "/mcp";
|
||||
private static final String DEFAULT_SERVER_NAME = "data-ai-backoffice";
|
||||
private static final String DEFAULT_TOOL_NAME = "oracle.select_ai.data_text2sql";
|
||||
private static final String DEFAULT_TOOL_LABEL = "업무 데이터 Text2SQL";
|
||||
private static final String DEFAULT_TOOL_DESCRIPTION =
|
||||
"승인된 업무 데이터용 읽기 전용 SELECT/WITH SQL을 생성하고, 검증 후 읽기 전용 "
|
||||
+ "트랜잭션에서 실행합니다.";
|
||||
private static final String DEFAULT_PROMPT_DESCRIPTION =
|
||||
"업무 데이터에서 조회할 내용을 자연어로 입력합니다.";
|
||||
|
||||
public String resolvedPublicUrl() {
|
||||
return requiredOrDefault(publicUrl, DEFAULT_PUBLIC_URL);
|
||||
}
|
||||
|
||||
public String resolvedServerName() {
|
||||
return requiredOrDefault(serverName, DEFAULT_SERVER_NAME);
|
||||
}
|
||||
|
||||
public String resolvedToolName() {
|
||||
return requiredOrDefault(toolName, DEFAULT_TOOL_NAME);
|
||||
}
|
||||
|
||||
public String resolvedToolLabel() {
|
||||
return requiredOrDefault(toolLabel, DEFAULT_TOOL_LABEL);
|
||||
}
|
||||
|
||||
public String resolvedToolDescription() {
|
||||
return requiredOrDefault(toolDescription, DEFAULT_TOOL_DESCRIPTION);
|
||||
}
|
||||
|
||||
public String resolvedPromptDescription() {
|
||||
return requiredOrDefault(promptDescription, DEFAULT_PROMPT_DESCRIPTION);
|
||||
}
|
||||
|
||||
private String requiredOrDefault(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class OrdsClientConfig {
|
||||
|
||||
@Bean
|
||||
RestTemplate ordsRestTemplate(BackofficeProperties properties) {
|
||||
Duration timeout = properties.ords().timeout();
|
||||
return new RestTemplateBuilder()
|
||||
.setConnectTimeout(timeout)
|
||||
.setReadTimeout(timeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestTemplate ordsAgentRestTemplate(BackofficeProperties properties) {
|
||||
Duration timeout = properties.ords().agentTimeout();
|
||||
return new RestTemplateBuilder()
|
||||
.setConnectTimeout(timeout)
|
||||
.setReadTimeout(timeout)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** Customer-facing labels that do not affect authorization or database identity. */
|
||||
@ConfigurationProperties(prefix = "backoffice.product")
|
||||
public record ProductProperties(String name, String title, String dataLabel) {
|
||||
|
||||
public String displayName() {
|
||||
return name == null || name.isBlank() ? "Data & AI Backoffice" : name.trim();
|
||||
}
|
||||
|
||||
public String pageTitle() {
|
||||
return title == null || title.isBlank() ? displayName() : title.trim();
|
||||
}
|
||||
|
||||
public String dataName() {
|
||||
return dataLabel == null || dataLabel.isBlank() ? "업무 데이터" : dataLabel.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
BackofficeProperties properties,
|
||||
UserDetailsService userDetailsService
|
||||
) throws Exception {
|
||||
if (properties.security().requireHttps()) {
|
||||
http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
|
||||
}
|
||||
|
||||
var security = properties.security();
|
||||
if (security.rememberMeConfigured()) {
|
||||
http.rememberMe(rememberMe -> rememberMe
|
||||
.key(security.rememberMeKey())
|
||||
.userDetailsService(userDetailsService)
|
||||
.rememberMeParameter("remember-me")
|
||||
.rememberMeCookieName("VPD_REMEMBER_ME")
|
||||
.tokenValiditySeconds(security.rememberMeValiditySeconds())
|
||||
.useSecureCookie(true)
|
||||
.alwaysRemember(true));
|
||||
}
|
||||
|
||||
return http
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(
|
||||
"/mcp", "/mcp/messages", "/mcp/*/messages"))
|
||||
.headers(headers -> headers.httpStrictTransportSecurity(hsts -> hsts
|
||||
.includeSubDomains(true)
|
||||
.maxAgeInSeconds(31_536_000)))
|
||||
.exceptionHandling(exceptions -> exceptions
|
||||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/css/**", "/js/**", "/webjars/**",
|
||||
"/mcp", "/mcp/sse", "/mcp/*/sse", "/mcp/messages", "/mcp/*/messages")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/login", "/logout").permitAll()
|
||||
.requestMatchers(HttpMethod.POST,
|
||||
"/probe",
|
||||
"/vector-knowledge/search",
|
||||
"/security-sql-scripts/explanation",
|
||||
"/mcp-chatbot",
|
||||
"/mcp-client-demo",
|
||||
"/mcp-reasoning")
|
||||
.authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/**").hasRole("ADMIN")
|
||||
.requestMatchers(HttpMethod.PATCH, "/**").hasRole("ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/**").hasRole("ADMIN")
|
||||
.requestMatchers(HttpMethod.DELETE, "/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated())
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login")
|
||||
.permitAll())
|
||||
.logout(logout -> logout.logoutSuccessUrl("/login?logout"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService(
|
||||
BackofficeProperties properties,
|
||||
PasswordEncoder passwordEncoder
|
||||
) {
|
||||
var security = properties.security();
|
||||
String encodedPassword = security.adminPasswordHash() == null || security.adminPasswordHash().isBlank()
|
||||
? passwordEncoder.encode(security.adminPassword())
|
||||
: security.adminPasswordHash();
|
||||
List<UserDetails> users = new ArrayList<>();
|
||||
users.add(User.withUsername(security.adminUser())
|
||||
.password(encodedPassword)
|
||||
.roles("ADMIN")
|
||||
.build());
|
||||
if (security.guestConfigured()) {
|
||||
String encodedGuestPassword = security.guestPasswordHash() == null || security.guestPasswordHash().isBlank()
|
||||
? passwordEncoder.encode(security.guestPassword())
|
||||
: security.guestPasswordHash();
|
||||
users.add(User.withUsername(security.guestUser())
|
||||
.password(encodedGuestPassword)
|
||||
.roles("VIEWER")
|
||||
.build());
|
||||
}
|
||||
return new InMemoryUserDetailsManager(users);
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** Deployment-provided allow-list for bundled security SQL shown by the backoffice. */
|
||||
@ConfigurationProperties(prefix = "backoffice.security-sql-scripts")
|
||||
public record SecuritySqlScriptProperties(String scripts) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.audit;
|
||||
|
||||
public record AuditEvent(
|
||||
String eventType,
|
||||
Long keyId,
|
||||
Long objectId,
|
||||
String status,
|
||||
Integer rowCount,
|
||||
String errorCode,
|
||||
String message
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.effective;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record EffectiveMatrixView(
|
||||
List<UserEffectiveAccessView> users,
|
||||
List<GroupEffectiveAccessView> groups,
|
||||
List<RoleEffectiveImpactView> roles,
|
||||
int permissionCount
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.effective;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record GroupEffectiveAccessView(
|
||||
long groupId,
|
||||
String groupCode,
|
||||
String groupName,
|
||||
boolean active,
|
||||
List<String> users,
|
||||
List<String> roles,
|
||||
int permissionCount,
|
||||
List<String> objectNames
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.effective;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RoleEffectiveImpactView(
|
||||
long roleId,
|
||||
String roleName,
|
||||
String maxSensitivityLevel,
|
||||
List<String> directUsers,
|
||||
List<String> groups,
|
||||
List<String> inheritedUsers,
|
||||
List<String> affectedUsers,
|
||||
int permissionCount,
|
||||
List<String> objectNames
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.effective;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record UserEffectiveAccessView(
|
||||
long userId,
|
||||
String username,
|
||||
String empNo,
|
||||
String deptCode,
|
||||
boolean active,
|
||||
List<String> directRoles,
|
||||
List<String> groups,
|
||||
List<String> inheritedRoles,
|
||||
List<String> effectiveRoles,
|
||||
int permissionCount,
|
||||
List<String> objectNames
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.group;
|
||||
|
||||
public record AppGroup(
|
||||
long groupId,
|
||||
String groupCode,
|
||||
String groupName,
|
||||
String description,
|
||||
String activeYn
|
||||
) {
|
||||
|
||||
public boolean active() {
|
||||
return "Y".equalsIgnoreCase(activeYn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.group;
|
||||
|
||||
public record GroupCreateCommand(
|
||||
String groupCode,
|
||||
String groupName,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.group;
|
||||
|
||||
public record GroupRoleView(
|
||||
long groupId,
|
||||
String groupCode,
|
||||
String groupName,
|
||||
long roleId,
|
||||
String roleName
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.group;
|
||||
|
||||
public record GroupUserView(
|
||||
long groupId,
|
||||
String groupCode,
|
||||
String groupName,
|
||||
long userId,
|
||||
String username
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record ColumnMaskingRule(
|
||||
long columnId,
|
||||
long objectId,
|
||||
String owner,
|
||||
String objectName,
|
||||
String columnName,
|
||||
long ruleId,
|
||||
String ruleCode,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String ruleEnabledYn
|
||||
) {
|
||||
|
||||
public String targetLabel() {
|
||||
return owner + "." + objectName + "." + columnName;
|
||||
}
|
||||
|
||||
public MaskingTemplate template() {
|
||||
return MaskingTemplate.from(templateCode);
|
||||
}
|
||||
|
||||
public String ruleLabel() {
|
||||
return ruleName + " · " + template().label();
|
||||
}
|
||||
|
||||
public boolean ruleEnabled() {
|
||||
return "Y".equalsIgnoreCase(ruleEnabledYn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
/**
|
||||
* Read-only comparison between the configured masking metadata and the
|
||||
* corresponding Oracle Data Redaction policy stored in the database.
|
||||
*/
|
||||
public record MaskingPolicyStatus(
|
||||
String owner,
|
||||
String objectName,
|
||||
String policyName,
|
||||
String enabled,
|
||||
int configuredColumnCount,
|
||||
int appliedColumnCount,
|
||||
int mismatchedColumnCount,
|
||||
int legacyVpdColumnPolicyCount
|
||||
) {
|
||||
|
||||
public String targetLabel() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public boolean policyEnabled() {
|
||||
return "YES".equalsIgnoreCase(enabled);
|
||||
}
|
||||
|
||||
/** A policy with no configured active column is correct only while disabled. */
|
||||
public boolean inactiveAsExpected() {
|
||||
return configuredColumnCount == 0 && !policyEnabled() && legacyVpdColumnPolicyCount == 0;
|
||||
}
|
||||
|
||||
public boolean applied() {
|
||||
return configuredColumnCount > 0
|
||||
&& policyEnabled()
|
||||
&& configuredColumnCount == appliedColumnCount
|
||||
&& mismatchedColumnCount == 0
|
||||
&& legacyVpdColumnPolicyCount == 0;
|
||||
}
|
||||
|
||||
public String statusLabel() {
|
||||
if (applied()) {
|
||||
return "적용됨";
|
||||
}
|
||||
if (inactiveAsExpected()) {
|
||||
return "미적용";
|
||||
}
|
||||
return "설정-DB 불일치";
|
||||
}
|
||||
|
||||
public String badgeClass() {
|
||||
if (applied()) {
|
||||
return "text-bg-success";
|
||||
}
|
||||
if (inactiveAsExpected()) {
|
||||
return "text-bg-secondary";
|
||||
}
|
||||
return "text-bg-danger";
|
||||
}
|
||||
|
||||
public String detail() {
|
||||
if (applied()) {
|
||||
return "활성 규칙과 DB 정책 컬럼이 일치합니다.";
|
||||
}
|
||||
if (legacyVpdColumnPolicyCount > 0) {
|
||||
return "기존 컬럼 NULL 정책(VPD CLS)이 활성입니다. ASO 설정과 별도로 값을 NULL 처리할 수 있습니다.";
|
||||
}
|
||||
if (inactiveAsExpected()) {
|
||||
return "활성 컬럼 기본 규칙이 없어 DB 정책을 중지 상태로 보관합니다.";
|
||||
}
|
||||
if (configuredColumnCount > 0 && !policyEnabled()) {
|
||||
return "활성 규칙이 있으나 DB 정책이 비활성입니다. 동기화가 필요합니다.";
|
||||
}
|
||||
if (configuredColumnCount == 0) {
|
||||
return "활성 규칙이 없는데 DB 정책이 활성입니다. 동기화가 필요합니다.";
|
||||
}
|
||||
return "설정 컬럼과 DB Redaction 컬럼이 다릅니다. 동기화가 필요합니다.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record MaskingRule(
|
||||
long ruleId,
|
||||
String ruleCode,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String description,
|
||||
String enabledYn
|
||||
) {
|
||||
|
||||
public boolean enabled() {
|
||||
return "Y".equalsIgnoreCase(enabledYn);
|
||||
}
|
||||
|
||||
public MaskingTemplate template() {
|
||||
return MaskingTemplate.from(templateCode);
|
||||
}
|
||||
|
||||
public String templateLabel() {
|
||||
return template().label();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record MaskingRuleCreateCommand(
|
||||
String ruleCode,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Curated Data Redaction behaviours. A rule selects one template; operators
|
||||
* never enter raw DBMS_REDACT expressions from the backoffice UI.
|
||||
*/
|
||||
public enum MaskingTemplate {
|
||||
NULLIFY(
|
||||
"NULLIFY",
|
||||
"값 숨김 (NULL)",
|
||||
"값을 NULL로 반환합니다. ASO/Data Redaction 컬럼 마스킹에 사용합니다.",
|
||||
"DBMS_REDACT.NULLIFY",
|
||||
"NULL"),
|
||||
FULL(
|
||||
"FULL",
|
||||
"전체 마스킹",
|
||||
"전체 값을 가립니다. Oracle 기본값은 문자형 공백, 숫자형 0입니다.",
|
||||
"DBMS_REDACT.FULL",
|
||||
"문자형은 공백 / 숫자형은 0"),
|
||||
TEXT_PARTIAL(
|
||||
"TEXT_PARTIAL",
|
||||
"문자열 일부 마스킹",
|
||||
"첫 글자만 남기고 나머지를 가리는 사전 정의 문자열 규칙입니다.",
|
||||
"DBMS_REDACT.REGEXP",
|
||||
"A******** (예시)"),
|
||||
RRN_PARTIAL(
|
||||
"RRN_PARTIAL",
|
||||
"주민등록번호 부분 마스킹",
|
||||
"앞 6자리만 표시하고 나머지는 가리는 사전 정의 식별번호 규칙입니다.",
|
||||
"DBMS_REDACT.REGEXP",
|
||||
"900101-******* (예시)");
|
||||
|
||||
private final String code;
|
||||
private final String label;
|
||||
private final String description;
|
||||
private final String asoFunction;
|
||||
private final String previewResult;
|
||||
|
||||
MaskingTemplate(String code, String label, String description, String asoFunction, String previewResult) {
|
||||
this.code = code;
|
||||
this.label = label;
|
||||
this.description = description;
|
||||
this.asoFunction = asoFunction;
|
||||
this.previewResult = previewResult;
|
||||
}
|
||||
|
||||
public String code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String asoFunction() {
|
||||
return asoFunction;
|
||||
}
|
||||
|
||||
/** Human-readable result shown before an administrator assigns the rule. */
|
||||
public String previewResult() {
|
||||
return previewResult;
|
||||
}
|
||||
|
||||
public static MaskingTemplate from(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(value -> value.code.equalsIgnoreCase(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 마스킹 템플릿입니다: " + code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record UserMaskingRule(
|
||||
long userId,
|
||||
String username,
|
||||
long columnId,
|
||||
String owner,
|
||||
String objectName,
|
||||
String columnName,
|
||||
String ruleName,
|
||||
String templateCode,
|
||||
String decision,
|
||||
String activeYn
|
||||
) {
|
||||
|
||||
public boolean unmasked() {
|
||||
return "UNMASK".equalsIgnoreCase(decision);
|
||||
}
|
||||
|
||||
public boolean active() {
|
||||
return "Y".equalsIgnoreCase(activeYn);
|
||||
}
|
||||
|
||||
public String decisionLabel() {
|
||||
return unmasked() ? "원문 표시 허용" : "기본값과 동일 · 마스킹";
|
||||
}
|
||||
|
||||
public String targetLabel() {
|
||||
return owner + "." + objectName + "." + columnName;
|
||||
}
|
||||
|
||||
public String ruleLabel() {
|
||||
return ruleName + " · " + MaskingTemplate.from(templateCode).label();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
public record McpChatbotResult(
|
||||
String contextPath,
|
||||
String question,
|
||||
String selectedTool,
|
||||
String status,
|
||||
String answer,
|
||||
String routingReason,
|
||||
String toolResultJson,
|
||||
String toolsListJson
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
public record McpClientDemoResult(
|
||||
String contextPath,
|
||||
String messageUrl,
|
||||
String selectedTool,
|
||||
String initializeResponse,
|
||||
String toolsListResponse,
|
||||
String toolsCallResponse,
|
||||
String status
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
public record McpReasoningCommand(
|
||||
Long tokenKeyId,
|
||||
String bearerToken,
|
||||
int limit,
|
||||
String question
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||
|
||||
public record McpReasoningResult(
|
||||
String modelStatus,
|
||||
String toolName,
|
||||
String answer,
|
||||
String prompt,
|
||||
String evidenceJson,
|
||||
ProbeResult probeResult
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
public record McpToolView(
|
||||
String name,
|
||||
String description,
|
||||
long objectId,
|
||||
String displayName,
|
||||
String ordsPath
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.operation;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Page-level health summary computed from the same DB-backed rows shown in
|
||||
* operation-status.html. It intentionally does not hide the row-level evidence:
|
||||
* the summary is only a triage layer for operators.
|
||||
*/
|
||||
public record OperationHealthSummary(
|
||||
int protectedObjectCount,
|
||||
int rowPolicyOkCount,
|
||||
int rowPolicyWarnCount,
|
||||
int rowPolicyErrorCount,
|
||||
int missingHandlerCount,
|
||||
int missingPolicyCount,
|
||||
int invalidFunctionCount,
|
||||
int unverifiedCount,
|
||||
int maskingPolicyCount,
|
||||
int maskingAppliedCount,
|
||||
int maskingInactiveExpectedCount,
|
||||
int maskingMismatchCount
|
||||
) {
|
||||
|
||||
public static OperationHealthSummary from(
|
||||
List<OperationStatusRow> rows,
|
||||
List<MaskingPolicyStatus> maskingStatuses
|
||||
) {
|
||||
int rowPolicyOk = 0;
|
||||
int rowPolicyWarn = 0;
|
||||
int rowPolicyError = 0;
|
||||
int missingHandler = 0;
|
||||
int missingPolicy = 0;
|
||||
int invalidFunction = 0;
|
||||
int unverified = 0;
|
||||
|
||||
for (OperationStatusRow row : rows) {
|
||||
switch (row.healthLevel()) {
|
||||
case "OK" -> rowPolicyOk++;
|
||||
case "ERROR" -> rowPolicyError++;
|
||||
default -> rowPolicyWarn++;
|
||||
}
|
||||
if (row.handlerId() == null) {
|
||||
missingHandler++;
|
||||
}
|
||||
if (row.policyNames() == null || row.policyNames().isBlank()) {
|
||||
missingPolicy++;
|
||||
}
|
||||
if (row.functionStatus() != null && !"VALID".equalsIgnoreCase(row.functionStatus())) {
|
||||
invalidFunction++;
|
||||
}
|
||||
if (row.lastProbeStatus() == null || row.lastProbeStatus().isBlank()) {
|
||||
unverified++;
|
||||
}
|
||||
}
|
||||
|
||||
int maskingApplied = 0;
|
||||
int maskingInactive = 0;
|
||||
int maskingMismatch = 0;
|
||||
for (MaskingPolicyStatus status : maskingStatuses) {
|
||||
if (status.applied()) {
|
||||
maskingApplied++;
|
||||
} else if (status.inactiveAsExpected()) {
|
||||
maskingInactive++;
|
||||
} else {
|
||||
maskingMismatch++;
|
||||
}
|
||||
}
|
||||
|
||||
return new OperationHealthSummary(
|
||||
rows.size(),
|
||||
rowPolicyOk,
|
||||
rowPolicyWarn,
|
||||
rowPolicyError,
|
||||
missingHandler,
|
||||
missingPolicy,
|
||||
invalidFunction,
|
||||
unverified,
|
||||
maskingStatuses.size(),
|
||||
maskingApplied,
|
||||
maskingInactive,
|
||||
maskingMismatch
|
||||
);
|
||||
}
|
||||
|
||||
public String overallLevel() {
|
||||
if (rowPolicyErrorCount > 0 || invalidFunctionCount > 0 || maskingMismatchCount > 0) {
|
||||
return "ERROR";
|
||||
}
|
||||
if (rowPolicyWarnCount > 0 || missingHandlerCount > 0 || missingPolicyCount > 0
|
||||
|| unverifiedCount > 0) {
|
||||
return "WARN";
|
||||
}
|
||||
return "OK";
|
||||
}
|
||||
|
||||
public String overallLabel() {
|
||||
return switch (overallLevel()) {
|
||||
case "ERROR" -> "조치 필요";
|
||||
case "WARN" -> "확인 필요";
|
||||
default -> "정상";
|
||||
};
|
||||
}
|
||||
|
||||
public String overallBadgeClass() {
|
||||
return switch (overallLevel()) {
|
||||
case "ERROR" -> "text-bg-danger";
|
||||
case "WARN" -> "text-bg-warning";
|
||||
default -> "text-bg-success";
|
||||
};
|
||||
}
|
||||
|
||||
public String rowPolicySummary() {
|
||||
return "정상 " + rowPolicyOkCount + " · 확인 " + rowPolicyWarnCount + " · 오류 " + rowPolicyErrorCount;
|
||||
}
|
||||
|
||||
public String maskingSummary() {
|
||||
return "적용 " + maskingAppliedCount + " · 미적용 정상 " + maskingInactiveExpectedCount
|
||||
+ " · 불일치 " + maskingMismatchCount;
|
||||
}
|
||||
|
||||
public String primaryAction() {
|
||||
if (invalidFunctionCount > 0) {
|
||||
return "컴파일 오류가 있는 VPD Filter를 먼저 확인하세요.";
|
||||
}
|
||||
if (maskingMismatchCount > 0) {
|
||||
return "컬럼 마스킹 화면에서 DB ASO 정책 동기화를 확인하세요.";
|
||||
}
|
||||
if (missingPolicyCount > 0) {
|
||||
return "보호 상태에서 행 접근 정책(VPD)을 적용하세요.";
|
||||
}
|
||||
if (missingHandlerCount > 0) {
|
||||
return "조회 대상 또는 조회 연동에서 ORDS handler를 확인하세요.";
|
||||
}
|
||||
if (unverifiedCount > 0) {
|
||||
return "접근 검증을 실행해 실제 결과를 확인하세요.";
|
||||
}
|
||||
return "현재 요약 기준으로 즉시 조치할 항목은 없습니다.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.operation;
|
||||
|
||||
public record OperationStatusRow(
|
||||
long objectId,
|
||||
String owner,
|
||||
String objectName,
|
||||
String ordsPath,
|
||||
String enabledYn,
|
||||
Integer permissionCount,
|
||||
Integer ruleCount,
|
||||
String policyNames,
|
||||
String policyEnabled,
|
||||
String functionName,
|
||||
String functionStatus,
|
||||
Long handlerId,
|
||||
String handlerMethod,
|
||||
String handlerSourceType,
|
||||
String handlerFullPath,
|
||||
String lastProbeStatus,
|
||||
Integer lastProbeRowCount,
|
||||
String lastProbeErrorCode,
|
||||
String lastProbeAt
|
||||
) {
|
||||
|
||||
public String displayName() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public String healthLevel() {
|
||||
if (functionStatus != null && !"VALID".equalsIgnoreCase(functionStatus)) {
|
||||
return "ERROR";
|
||||
}
|
||||
if (handlerId == null || policyNames == null || policyNames.isBlank()
|
||||
|| policyEnabled == null || !policyEnabled.toUpperCase().contains("YES")) {
|
||||
return "WARN";
|
||||
}
|
||||
if (lastProbeStatus != null && !lastProbeStatus.isBlank()
|
||||
&& !lastProbeStatus.toUpperCase().contains("SUCCESS")
|
||||
&& !lastProbeStatus.toUpperCase().contains("ALLOW")) {
|
||||
return "WARN";
|
||||
}
|
||||
return "OK";
|
||||
}
|
||||
|
||||
public String actionText() {
|
||||
if (handlerId == null) {
|
||||
return "ORDS path와 handler schema/module/template 매핑을 확인하세요.";
|
||||
}
|
||||
if (policyNames == null || policyNames.isBlank()) {
|
||||
return "행 접근 정책(VPD)을 적용하세요.";
|
||||
}
|
||||
if (policyEnabled == null || !policyEnabled.toUpperCase().contains("YES")) {
|
||||
return "행 접근 정책(VPD) enable 상태를 확인하세요.";
|
||||
}
|
||||
if (functionStatus != null && !"VALID".equalsIgnoreCase(functionStatus)) {
|
||||
return "Policy function 컴파일 오류를 확인하세요.";
|
||||
}
|
||||
if (lastProbeStatus == null || lastProbeStatus.isBlank()) {
|
||||
return "ORDS 검증을 한 번 실행하세요.";
|
||||
}
|
||||
return "현재 상태에서 즉시 조치할 항목은 없습니다.";
|
||||
}
|
||||
|
||||
public String permissionSummary() {
|
||||
int permissions = permissionCount == null ? 0 : permissionCount;
|
||||
int rules = ruleCount == null ? 0 : ruleCount;
|
||||
return permissions + " permissions / " + rules + " rules";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.ords;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record OrdsHandlerUpdateCommand(
|
||||
long handlerId,
|
||||
@NotBlank String source
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.ords;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public record OrdsHandlerView(
|
||||
Long handlerId,
|
||||
String schemaName,
|
||||
String parsingSchema,
|
||||
String moduleName,
|
||||
String basePath,
|
||||
String template,
|
||||
String method,
|
||||
String sourceType,
|
||||
String source,
|
||||
String parameters,
|
||||
String packageSource
|
||||
) {
|
||||
|
||||
public String fullPath() {
|
||||
return "/ords/" + schemaName + "/" + basePath + template;
|
||||
}
|
||||
|
||||
public boolean plsqlEditable() {
|
||||
return sourceType != null && sourceType.toLowerCase(Locale.ROOT).contains("plsql");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.ords;
|
||||
|
||||
public record OrdsObjectHandlerResult(
|
||||
long objectId,
|
||||
String ordsPath,
|
||||
String moduleName,
|
||||
String template
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
public record AppRole(
|
||||
long roleId,
|
||||
String roleName,
|
||||
String description,
|
||||
String maxSensitivityLevel
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
public record PermissionRule(
|
||||
long ruleId,
|
||||
long permissionId,
|
||||
String ruleColumn,
|
||||
String ruleType,
|
||||
String ruleValue
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PermissionSet(
|
||||
long permissionId,
|
||||
long roleId,
|
||||
long objectId,
|
||||
String action,
|
||||
String permissionEffect,
|
||||
List<PermissionRule> rules,
|
||||
List<String> visibleColumns
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import java.util.List;
|
||||
|
||||
public record PermissionSetCommand(
|
||||
@Positive long roleId,
|
||||
@Positive long objectId,
|
||||
@NotBlank String action,
|
||||
String permissionEffect,
|
||||
@NotEmpty List<RuleCommand> rules,
|
||||
List<String> visibleColumns
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
public record PermissionView(
|
||||
long permissionId,
|
||||
long roleId,
|
||||
String roleName,
|
||||
long objectId,
|
||||
String objectName,
|
||||
String action,
|
||||
String permissionEffect,
|
||||
String rules,
|
||||
String visibleColumns,
|
||||
String filterPreview,
|
||||
String nullPolicyPreview
|
||||
) {
|
||||
|
||||
public String businessRuleSummary() {
|
||||
if (rules == null || rules.isBlank()) {
|
||||
return "-";
|
||||
}
|
||||
return java.util.Arrays.stream(rules.split(","))
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isBlank())
|
||||
.map(PermissionView::businessRuleLabel)
|
||||
.collect(java.util.stream.Collectors.joining(System.lineSeparator() + "AND "));
|
||||
}
|
||||
|
||||
public String storedRuleSummary() {
|
||||
return rules == null || rules.isBlank() ? "-" : rules;
|
||||
}
|
||||
|
||||
public String vpdMappingSummary() {
|
||||
return filterPreview == null || filterPreview.isBlank() ? "-" : filterPreview;
|
||||
}
|
||||
|
||||
public String columnControlSummary() {
|
||||
if (visibleColumns == null || visibleColumns.isBlank()) {
|
||||
return "컬럼 원문/마스킹은 컬럼 마스킹에서 관리";
|
||||
}
|
||||
return "기존 권한별 표시 예외 값: " + visibleColumns
|
||||
+ " · 신규 컬럼 제어는 컬럼 마스킹에서 관리";
|
||||
}
|
||||
|
||||
private static String businessRuleLabel(String rawRule) {
|
||||
String upper = rawRule.toUpperCase(java.util.Locale.ROOT);
|
||||
if ("ALL".equals(upper)) {
|
||||
return "전체 행";
|
||||
}
|
||||
if (upper.contains(" TOKEN_SUBJECT")) {
|
||||
return "토큰으로 식별된 이해관계자 본인 행";
|
||||
}
|
||||
if (upper.contains(" OWN_CONTRACT")) {
|
||||
return "담당 설계사 본인 계약";
|
||||
}
|
||||
if (upper.contains(" CHANNEL_CONTRACT")) {
|
||||
return "토큰 사용자의 채널 계약";
|
||||
}
|
||||
if (upper.contains(" OWN_CUSTOMER")) {
|
||||
return "담당 설계사 본인 계약에 연결된 고객/청구/외부보유";
|
||||
}
|
||||
if (upper.contains(" CHANNEL_CUSTOMER")) {
|
||||
return "토큰 사용자 채널 계약에 연결된 고객/청구/외부보유";
|
||||
}
|
||||
if (upper.contains(" STATIC_SQL ")) {
|
||||
return "정적 SQL 조건: " + rawRule.replaceFirst("(?i)^\\s*STATIC_SQL\\s+", "");
|
||||
}
|
||||
if (upper.startsWith("STATIC_SQL ")) {
|
||||
return "정적 SQL 조건: " + rawRule.substring("STATIC_SQL ".length());
|
||||
}
|
||||
if ("MY_DEPT".equals(upper) || upper.contains(" MY_DEPT")) {
|
||||
return "내 부서 행";
|
||||
}
|
||||
if ("MANAGED_TEAM".equals(upper) || upper.contains(" MANAGED_TEAM")) {
|
||||
return "본인 및 직접 보고 팀원 행";
|
||||
}
|
||||
if ("SELF".equals(upper) || upper.contains(" SELF")) {
|
||||
return "본인 직원 행";
|
||||
}
|
||||
if (upper.contains(" TAG ")) {
|
||||
return "태그 조건: " + rawRule;
|
||||
}
|
||||
return rawRule;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record RuleCommand(
|
||||
String ruleColumn,
|
||||
@NotBlank String ruleType,
|
||||
String ruleValue
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
/**
|
||||
* Durable, database-generated FGA evidence for a protected-object SELECT.
|
||||
* SQL text and RLS information come from Oracle's audit trail, not from a
|
||||
* re-evaluation of the VPD policy function.
|
||||
*/
|
||||
public record FgaExecutionEvidence(
|
||||
String eventAt,
|
||||
String dbUser,
|
||||
String clientId,
|
||||
String statementType,
|
||||
String sqlText,
|
||||
String rlsInfo
|
||||
) {
|
||||
|
||||
public boolean hasRlsInfo() {
|
||||
return rlsInfo != null && !rlsInfo.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
public record ProbeCommand(
|
||||
Long tokenKeyId,
|
||||
@Positive long objectId,
|
||||
@NotBlank String bearerToken,
|
||||
@Min(1) @Max(500) int limit,
|
||||
String requestBody
|
||||
) {
|
||||
|
||||
public ProbeCommand(Long tokenKeyId, long objectId, String bearerToken, int limit) {
|
||||
this(tokenKeyId, objectId, bearerToken, limit, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record ProbeResult(
|
||||
ProbeStatus status,
|
||||
List<String> columns,
|
||||
List<Map<String, Object>> rows,
|
||||
int rowCount,
|
||||
List<String> maskedColumns,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String requestHeaders,
|
||||
String requestPayload,
|
||||
String responseHeaders,
|
||||
String responseBody,
|
||||
String vpdPredicate,
|
||||
String effectiveSql,
|
||||
SqlExecutionEvidence executionEvidence,
|
||||
String executionEvidenceMessage,
|
||||
List<SqlExecutionCandidate> executionCandidates,
|
||||
FgaExecutionEvidence fgaExecutionEvidence,
|
||||
String fgaExecutionEvidenceMessage
|
||||
) {
|
||||
|
||||
public ProbeResult(
|
||||
ProbeStatus status,
|
||||
List<String> columns,
|
||||
List<Map<String, Object>> rows,
|
||||
int rowCount,
|
||||
List<String> maskedColumns,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String requestHeaders,
|
||||
String requestPayload,
|
||||
String responseHeaders,
|
||||
String responseBody
|
||||
) {
|
||||
this(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public ProbeResult(
|
||||
ProbeStatus status,
|
||||
List<String> columns,
|
||||
List<Map<String, Object>> rows,
|
||||
int rowCount,
|
||||
List<String> maskedColumns,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String requestHeaders,
|
||||
String requestPayload,
|
||||
String responseHeaders,
|
||||
String responseBody,
|
||||
String vpdPredicate,
|
||||
String effectiveSql
|
||||
) {
|
||||
this(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
vpdPredicate,
|
||||
effectiveSql,
|
||||
null,
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static ProbeResult blocked(ProbeStatus status, String errorCode, String errorMessage) {
|
||||
return blocked(status, errorCode, errorMessage, null, null, null, null);
|
||||
}
|
||||
|
||||
public static ProbeResult blocked(
|
||||
ProbeStatus status,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String requestHeaders,
|
||||
String requestPayload,
|
||||
String responseHeaders,
|
||||
String responseBody
|
||||
) {
|
||||
return new ProbeResult(
|
||||
status,
|
||||
List.of(),
|
||||
List.of(),
|
||||
0,
|
||||
List.of(),
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody
|
||||
);
|
||||
}
|
||||
|
||||
public boolean successLike() {
|
||||
return status == ProbeStatus.SUCCESS || status == ProbeStatus.VPD_DENY_EMPTY_RESULT;
|
||||
}
|
||||
|
||||
public boolean hasSqlTrace() {
|
||||
return effectiveSql != null && !effectiveSql.isBlank();
|
||||
}
|
||||
|
||||
public ProbeResult withSqlTrace(String predicate, String sql) {
|
||||
return new ProbeResult(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
predicate,
|
||||
sql,
|
||||
executionEvidence,
|
||||
executionEvidenceMessage,
|
||||
executionCandidates,
|
||||
fgaExecutionEvidence,
|
||||
fgaExecutionEvidenceMessage
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasExecutionEvidence() {
|
||||
return executionEvidence != null && executionEvidence.sqlId() != null
|
||||
&& !executionEvidence.sqlId().isBlank();
|
||||
}
|
||||
|
||||
public ProbeResult withExecutionEvidence(
|
||||
SqlExecutionEvidence evidence,
|
||||
String unavailableMessage
|
||||
) {
|
||||
return withExecutionEvidence(evidence, unavailableMessage, List.of());
|
||||
}
|
||||
|
||||
public ProbeResult withExecutionEvidence(
|
||||
SqlExecutionEvidence evidence,
|
||||
String unavailableMessage,
|
||||
List<SqlExecutionCandidate> candidates
|
||||
) {
|
||||
return new ProbeResult(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
vpdPredicate,
|
||||
effectiveSql,
|
||||
evidence,
|
||||
unavailableMessage,
|
||||
candidates == null ? List.of() : List.copyOf(candidates),
|
||||
fgaExecutionEvidence,
|
||||
fgaExecutionEvidenceMessage
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasExecutionCandidates() {
|
||||
return executionCandidates != null && !executionCandidates.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFgaExecutionEvidence() {
|
||||
return fgaExecutionEvidence != null && fgaExecutionEvidence.sqlText() != null
|
||||
&& !fgaExecutionEvidence.sqlText().isBlank();
|
||||
}
|
||||
|
||||
public ProbeResult withFgaExecutionEvidence(
|
||||
FgaExecutionEvidence evidence,
|
||||
String unavailableMessage
|
||||
) {
|
||||
return new ProbeResult(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
vpdPredicate,
|
||||
effectiveSql,
|
||||
executionEvidence,
|
||||
executionEvidenceMessage,
|
||||
executionCandidates,
|
||||
evidence,
|
||||
unavailableMessage
|
||||
);
|
||||
}
|
||||
|
||||
public String title() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "권한에 따라 데이터를 볼 수 있습니다.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "현재 권한으로 볼 수 있는 행이 없습니다.";
|
||||
case TOKEN_NOT_FOUND -> "DB에 등록되지 않은 토큰입니다.";
|
||||
case TOKEN_INACTIVE -> "만료되었거나 회수된 토큰입니다.";
|
||||
case INVALID_TOKEN -> "입력한 토큰 정보가 일치하지 않습니다.";
|
||||
case OBJECT_DISABLED -> "검증 대상이 비활성 상태입니다.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "현재 권한으로 이 대상에 접근할 수 없습니다.";
|
||||
case VPD_FILTER_ERROR -> "이 객체에 연결된 행 접근 Filter가 실행되지 않습니다.";
|
||||
case ORDS_PATH_NOT_FOUND -> "검증 대상의 ORDS 경로를 찾지 못했습니다.";
|
||||
case ORDS_NOT_CONFIGURED -> "ORDS 연결 주소가 아직 설정되지 않았습니다.";
|
||||
case ORDS_UNAVAILABLE -> "ORDS 서버에 연결할 수 없습니다.";
|
||||
case ORDS_TIMEOUT -> "ORDS 응답을 기다리다 시간이 초과되었습니다.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS 응답 형식을 해석할 수 없습니다.";
|
||||
case UNKNOWN_ERROR -> "검증 중 예상하지 못한 문제가 발생했습니다.";
|
||||
};
|
||||
}
|
||||
|
||||
public String plainSummary() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "토큰의 사용자와 역할을 기준으로 행 접근 정책이 적용되었고, 허용된 데이터 " + rowCount
|
||||
+ "개가 반환되었습니다.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "호출은 정상 처리됐지만 행 접근 정책이 현재 사용자에게 허용한 행은 0개입니다. 행 접근 규칙과 실제 데이터가 맞지 않으면 정상 결과이며 오류가 아닐 수 있습니다.";
|
||||
case TOKEN_NOT_FOUND -> "입력한 원문과 일치하는 등록 기록이 현재 DB에 없습니다. 예전에 발급한 값이거나 다른 환경의 토큰일 수 있습니다.";
|
||||
case TOKEN_INACTIVE -> "토큰은 DB에 있지만 만료되었거나 관리자가 회수해 더 이상 사용자 권한을 증명할 수 없습니다.";
|
||||
case INVALID_TOKEN -> "화면에서 선택한 정보와 입력한 토큰 원문이 서로 다릅니다.";
|
||||
case OBJECT_DISABLED -> "선택한 객체가 검증 대상으로 활성화되어 있지 않아 ORDS 호출을 시작하지 않았습니다.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "토큰은 확인됐지만 ORDS 또는 DB가 이 객체에 대한 접근을 거부했습니다.";
|
||||
case VPD_FILTER_ERROR -> "토큰과 사용자 권한은 확인됐지만, 대상 객체의 VPD 함수가 DB 오류를 내어 행 필터를 계산하지 못했습니다.";
|
||||
case ORDS_PATH_NOT_FOUND -> "권한 판단 전 단계에서 실제 ORDS 주소와 등록된 객체 경로가 일치하지 않았습니다.";
|
||||
case ORDS_NOT_CONFIGURED -> "백오피스가 호출할 ORDS 기준 주소가 없어 권한 검증을 시작하지 못했습니다.";
|
||||
case ORDS_UNAVAILABLE -> "권한 판단 전 단계에서 ORDS 서버 또는 네트워크에 연결하지 못했습니다.";
|
||||
case ORDS_TIMEOUT -> "ORDS가 제한 시간 안에 응답하지 않아 권한 결과를 확인하지 못했습니다.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS 호출은 끝났지만 rows/items 배열이 없는 응답이라 권한 결과로 표시하지 못했습니다.";
|
||||
case UNKNOWN_ERROR -> "토큰, 권한, ORDS 중 어느 단계의 문제인지 기술 상세를 확인해야 합니다.";
|
||||
};
|
||||
}
|
||||
|
||||
public String nextAction() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "반환된 행과 ASO 마스킹 컬럼이 예상한 범위인지 확인하세요. 행 범위가 다르면 행 접근 규칙을, 컬럼 표시가 다르면 컬럼 마스킹을 조정한 뒤 다시 검증하세요.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "유효 권한 화면에서 사용자에게 직접 또는 그룹으로 상속된 역할과 행 규칙을 확인하세요.";
|
||||
case TOKEN_NOT_FOUND -> "토큰 화면에서 현재 환경의 사용자에게 새 토큰을 발급하고, 한 번만 표시되는 원문을 복사해 다시 검증하세요.";
|
||||
case TOKEN_INACTIVE -> "토큰 화면에서 활성 토큰을 새로 발급한 뒤 다시 검증하세요.";
|
||||
case INVALID_TOKEN -> "복사한 원문이 맞는지 확인하고, 원문을 잃었다면 새 토큰을 발급하세요.";
|
||||
case OBJECT_DISABLED -> "보호 객체를 활성화하고 권한을 등록한 뒤 다시 검증하세요.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "유효 권한과 ORDS handler의 대상 객체가 같은지 확인하세요.";
|
||||
case VPD_FILTER_ERROR -> "토큰이나 행 접근 규칙을 바꾸지 말고 DB 보호 연결에서 이 객체의 Filter를 확인하세요. 일반 권한 객체라면 표준 행 접근 Filter로 복구하세요.";
|
||||
case ORDS_PATH_NOT_FOUND -> "보호 객체의 ORDS 경로와 실제 module/template 경로를 맞춘 뒤 다시 실행하세요.";
|
||||
case ORDS_NOT_CONFIGURED -> "설정에서 ORDS 기준 주소를 등록한 뒤 백오피스를 재시작하세요.";
|
||||
case ORDS_UNAVAILABLE, ORDS_TIMEOUT -> "행 접근 규칙을 바꾸지 말고 먼저 ORDS 실행 상태, 주소와 네트워크를 확인하세요.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS handler가 rows 또는 items 배열을 반환하는지 확인하세요.";
|
||||
case UNKNOWN_ERROR -> "아래 기술 상세의 오류 코드와 응답을 확인한 뒤 해당 단계부터 점검하세요.";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
public enum ProbeStatus {
|
||||
SUCCESS,
|
||||
VPD_DENY_EMPTY_RESULT,
|
||||
TOKEN_NOT_FOUND,
|
||||
TOKEN_INACTIVE,
|
||||
OBJECT_DISABLED,
|
||||
INVALID_TOKEN,
|
||||
OBJECT_NOT_ACCESSIBLE,
|
||||
VPD_FILTER_ERROR,
|
||||
ORDS_PATH_NOT_FOUND,
|
||||
ORDS_NOT_CONFIGURED,
|
||||
ORDS_UNAVAILABLE,
|
||||
ORDS_TIMEOUT,
|
||||
INVALID_ORDS_RESPONSE,
|
||||
UNKNOWN_ERROR
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
/**
|
||||
* One of the most recently active database cursors shown verbatim during a
|
||||
* probe. The match flag is calculated in Java, not by SQL text filtering in
|
||||
* the database.
|
||||
*/
|
||||
public record SqlExecutionCandidate(
|
||||
String sqlId,
|
||||
int childNumber,
|
||||
String parsingSchema,
|
||||
String originalSql,
|
||||
String lastActiveAt,
|
||||
boolean matchesProtectedObject
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
/**
|
||||
* Database shared-pool evidence for the statement most recently executed for
|
||||
* a protected object. The SQL text is the statement submitted by ORDS before
|
||||
* VPD rewrite; Oracle exposes the applied predicate through the cursor plan.
|
||||
*/
|
||||
public record SqlExecutionEvidence(
|
||||
String sqlId,
|
||||
int childNumber,
|
||||
String originalSql,
|
||||
String lastActiveAt,
|
||||
long executions,
|
||||
long rowsProcessed,
|
||||
long elapsedMillis,
|
||||
long bufferGets,
|
||||
String predicatePlan
|
||||
) {
|
||||
|
||||
public boolean hasPredicatePlan() {
|
||||
return predicatePlan != null && !predicatePlan.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.protectedobject;
|
||||
|
||||
public record DatabaseObjectOption(
|
||||
String owner,
|
||||
String objectName,
|
||||
String objectType
|
||||
) {
|
||||
|
||||
public String value() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return owner + "." + objectName + " (" + objectType + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.protectedobject;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public record ProtectedColumn(
|
||||
long columnId,
|
||||
long objectId,
|
||||
String columnName,
|
||||
String sensitiveYn,
|
||||
Long visibleRoleId,
|
||||
String sensitivityLevel,
|
||||
String redactionMethod
|
||||
) {
|
||||
|
||||
public boolean sensitive() {
|
||||
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;
|
||||
}
|
||||
|
||||
public String sensitivityLabel() {
|
||||
String level = sensitivityLevel == null || sensitivityLevel.isBlank() ? "PUBLIC" : sensitivityLevel;
|
||||
return switch (level.toUpperCase(Locale.ROOT)) {
|
||||
case "INTERNAL" -> "내부용";
|
||||
case "CONFIDENTIAL" -> "기밀";
|
||||
case "RESTRICTED" -> "제한";
|
||||
default -> "기본 표시";
|
||||
};
|
||||
}
|
||||
|
||||
public String redactionLabel() {
|
||||
String method = redactionMethod == null || redactionMethod.isBlank() ? "NONE" : redactionMethod;
|
||||
return switch (method.toUpperCase(Locale.ROOT)) {
|
||||
case "NULLIFY" -> "값 숨김(NULL)";
|
||||
case "PARTIAL" -> "일부 숨김";
|
||||
case "FULL" -> "전체 숨김";
|
||||
default -> "마스킹 없음";
|
||||
};
|
||||
}
|
||||
|
||||
public String displayPolicyLabel() {
|
||||
return sensitive() ? sensitivityLabel() + " · " + redactionLabel() : "기본 표시 · 마스킹 없음";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.protectedobject;
|
||||
|
||||
public record ProtectedObject(
|
||||
long objectId,
|
||||
String owner,
|
||||
String objectName,
|
||||
String ordsPath,
|
||||
String enabledYn,
|
||||
String description
|
||||
) {
|
||||
|
||||
public ProtectedObject(long objectId, String owner, String objectName, String ordsPath, String enabledYn) {
|
||||
this(objectId, owner, objectName, ordsPath, enabledYn, null);
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return "Y".equalsIgnoreCase(enabledYn);
|
||||
}
|
||||
|
||||
public String displayName() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public String descriptionOrDefault() {
|
||||
return description == null || description.isBlank()
|
||||
? "등록된 DB 객체를 권한체계와 ORDS로 조회하는 대상"
|
||||
: description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.protectedobject;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ProtectedObjectCreateCommand(
|
||||
@NotBlank String owner,
|
||||
@NotBlank String objectName,
|
||||
@NotBlank String ordsPath,
|
||||
String columns,
|
||||
String sensitiveColumns,
|
||||
String description
|
||||
) {
|
||||
|
||||
public ProtectedObjectCreateCommand(
|
||||
String owner,
|
||||
String objectName,
|
||||
String ordsPath,
|
||||
String columns,
|
||||
String sensitiveColumns
|
||||
) {
|
||||
this(owner, objectName, ordsPath, columns, sensitiveColumns, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schema;
|
||||
|
||||
public record SchemaActionResult(
|
||||
String objectName,
|
||||
String action,
|
||||
String status,
|
||||
String message,
|
||||
String sqlText
|
||||
) {
|
||||
|
||||
public SchemaActionResult(String objectName, String action, String status, String message) {
|
||||
this(objectName, action, status, message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schema;
|
||||
|
||||
public record SchemaPreflightCheck(
|
||||
String layer,
|
||||
String itemName,
|
||||
String objectType,
|
||||
String status,
|
||||
String detail,
|
||||
String nextAction,
|
||||
boolean sqlclRequired
|
||||
) {
|
||||
|
||||
public boolean ok() {
|
||||
return "OK".equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
public boolean warning() {
|
||||
return "WARN".equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
public boolean missing() {
|
||||
return "MISSING".equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
public boolean error() {
|
||||
return "ERROR".equalsIgnoreCase(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SchemaPreflightView(
|
||||
String currentUser,
|
||||
String recommendedOrdsSchema,
|
||||
List<SchemaPreflightCheck> checks,
|
||||
String appDdlPreview,
|
||||
String sqlclScript
|
||||
) {
|
||||
|
||||
public long okCount() {
|
||||
return count("OK");
|
||||
}
|
||||
|
||||
public long warnCount() {
|
||||
return count("WARN");
|
||||
}
|
||||
|
||||
public long missingCount() {
|
||||
return count("MISSING");
|
||||
}
|
||||
|
||||
public long errorCount() {
|
||||
return count("ERROR");
|
||||
}
|
||||
|
||||
public boolean hasActionNeeded() {
|
||||
return warnCount() + missingCount() + errorCount() > 0;
|
||||
}
|
||||
|
||||
private long count(String status) {
|
||||
return checks.stream()
|
||||
.filter(check -> status.equalsIgnoreCase(check.status()))
|
||||
.count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
public record SchemaAnnotation(String name, String value) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SchemaMetadataColumn(
|
||||
String columnName,
|
||||
String dataType,
|
||||
boolean nullable,
|
||||
String comment,
|
||||
List<SchemaAnnotation> annotations
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import java.util.List;
|
||||
|
||||
public record SchemaMetadataView(
|
||||
StructuredDataTable table,
|
||||
String tableComment,
|
||||
List<SchemaAnnotation> tableAnnotations,
|
||||
List<SchemaMetadataColumn> columns
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.securityscript;
|
||||
|
||||
/** Curated, version-controlled database script displayed read-only in the backoffice. */
|
||||
public record SecuritySqlScript(
|
||||
String scriptId,
|
||||
String category,
|
||||
String fileName,
|
||||
String title,
|
||||
String description,
|
||||
String source
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.securityscript;
|
||||
|
||||
/** Explanation generated from one immutable, curated security SQL script. */
|
||||
public record SecuritySqlScriptExplanation(
|
||||
String status,
|
||||
String modelName,
|
||||
String answer,
|
||||
String prompt,
|
||||
SecuritySqlScript script
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.securityscript;
|
||||
|
||||
/** Metadata for selecting a curated security SQL script without loading its source. */
|
||||
public record SecuritySqlScriptSummary(
|
||||
String scriptId,
|
||||
String category,
|
||||
String fileName,
|
||||
String title,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.setting;
|
||||
|
||||
public record BackofficeSetting(
|
||||
String settingKey,
|
||||
String settingValue
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.stakeholder;
|
||||
|
||||
/**
|
||||
* 업무 사용자 원장과 백오피스 권한 bridge를 결합한 Bearer Token 발급 대상이다.
|
||||
*/
|
||||
public record StakeholderTokenSubject(
|
||||
String stakeholderUserId,
|
||||
String username,
|
||||
String role,
|
||||
String channel,
|
||||
String accessScope,
|
||||
long appUserId
|
||||
) {
|
||||
|
||||
public String displayLabel() {
|
||||
return username + " / " + stakeholderUserId + " / " + role + " / " + channel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.structured;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record StructuredDataPreview(
|
||||
StructuredDataTable table,
|
||||
List<String> columns,
|
||||
List<Map<String, Object>> rows,
|
||||
int rowLimit
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.structured;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record StructuredDataTable(
|
||||
String key,
|
||||
String tableName,
|
||||
String objectType,
|
||||
String businessName,
|
||||
String description,
|
||||
List<String> previewColumns
|
||||
) {
|
||||
|
||||
public StructuredDataTable(String key, String tableName, String businessName, String description) {
|
||||
this(key, tableName, "TABLE", businessName, description, List.of());
|
||||
}
|
||||
|
||||
public boolean isTable() {
|
||||
return "TABLE".equals(objectType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.token;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record BearerTokenRecord(
|
||||
long keyId,
|
||||
long userId,
|
||||
String username,
|
||||
String stakeholderUserId,
|
||||
String stakeholderRole,
|
||||
String stakeholderChannel,
|
||||
String keyPrefix,
|
||||
String keyHash,
|
||||
LocalDateTime expiresAt,
|
||||
LocalDateTime revokedAt,
|
||||
String description
|
||||
) {
|
||||
|
||||
public boolean active(LocalDateTime now) {
|
||||
return revokedAt == null && expiresAt != null && expiresAt.isAfter(now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.token;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record IssuedToken(
|
||||
long keyId,
|
||||
String prefix,
|
||||
String plainToken,
|
||||
OffsetDateTime expiresAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.token;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record TokenContextView(
|
||||
long keyId,
|
||||
long userId,
|
||||
String username,
|
||||
String stakeholderUserId,
|
||||
String stakeholderRole,
|
||||
String stakeholderChannel,
|
||||
String keyPrefix,
|
||||
LocalDateTime expiresAt,
|
||||
LocalDateTime revokedAt,
|
||||
String description,
|
||||
boolean active,
|
||||
List<String> directRoles,
|
||||
List<String> groups,
|
||||
List<String> inheritedRoles
|
||||
) {
|
||||
|
||||
public String statusLabel() {
|
||||
if (active) {
|
||||
return "ACTIVE";
|
||||
}
|
||||
return revokedAt == null ? "EXPIRED" : "REVOKED";
|
||||
}
|
||||
|
||||
public String maskedToken() {
|
||||
if (keyPrefix == null || keyPrefix.isBlank()) {
|
||||
return "****";
|
||||
}
|
||||
return keyPrefix + "****";
|
||||
}
|
||||
|
||||
public String displayLabel() {
|
||||
return "#" + keyId + " / " + username + " / " + statusLabel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.token;
|
||||
|
||||
import jakarta.validation.constraints.Future;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record TokenIssueCommand(
|
||||
@Positive long userId,
|
||||
@Future OffsetDateTime expiresAt,
|
||||
@Size(max = 200) String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.user;
|
||||
|
||||
public record AppUser(
|
||||
long userId,
|
||||
String username,
|
||||
String empNo,
|
||||
String deptCode,
|
||||
String canReadContents,
|
||||
String activeYn
|
||||
) {
|
||||
|
||||
public boolean active() {
|
||||
return "Y".equalsIgnoreCase(activeYn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.user;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record UserCreateCommand(
|
||||
@NotBlank String username,
|
||||
@NotBlank String empNo,
|
||||
@NotBlank String deptCode
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.user;
|
||||
|
||||
public record UserRoleView(
|
||||
long userId,
|
||||
String username,
|
||||
long roleId,
|
||||
String roleName
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vector;
|
||||
|
||||
public record VectorChunk(int chunkNo, String text) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vector;
|
||||
|
||||
public record VectorIngestCommand(
|
||||
String documentId,
|
||||
String title,
|
||||
String sourceUri,
|
||||
String content,
|
||||
String techTags,
|
||||
int chunkSize,
|
||||
String embeddingMode
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vector;
|
||||
|
||||
public record VectorIngestResult(
|
||||
String documentId,
|
||||
int chunkCount,
|
||||
int tagCount,
|
||||
String embeddingMode,
|
||||
String embeddingModel
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vector;
|
||||
|
||||
public record VectorKnowledgeSummary(
|
||||
int documentCount,
|
||||
int chunkCount,
|
||||
int tagCount,
|
||||
boolean vectorObjectRegistered,
|
||||
String embeddingModel
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vector;
|
||||
|
||||
public record VectorQueryEmbedding(
|
||||
String query,
|
||||
String embeddingMode,
|
||||
String embeddingModel,
|
||||
String requestBody
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vector;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||
|
||||
public record VectorSearchResult(
|
||||
String query,
|
||||
String embeddingMode,
|
||||
String embeddingModel,
|
||||
ProbeResult probe
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdBulkApplyResult(
|
||||
int total,
|
||||
int created,
|
||||
int skipped,
|
||||
int failed
|
||||
) {
|
||||
|
||||
public String summary() {
|
||||
return "VPD bulk 적용 완료: 대상 " + total + "개, 등록 " + created
|
||||
+ "개, 건너뜀 " + skipped + "개, 실패 " + failed + "개";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
/** A saved VPD policy or filter description, addressed by a stable composite key. */
|
||||
public record VpdDescriptionNote(String noteKey, String description) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdFunctionOption(
|
||||
String owner,
|
||||
String packageName,
|
||||
String functionName,
|
||||
String objectType
|
||||
) {
|
||||
|
||||
public String value() {
|
||||
String packagePrefix = packageName == null || packageName.isBlank() ? "" : packageName + ".";
|
||||
return owner + "." + packagePrefix + functionName;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return value() + " / " + objectType;
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdFunctionSource(
|
||||
String owner,
|
||||
String objectName,
|
||||
String objectType,
|
||||
String source
|
||||
) {
|
||||
|
||||
public boolean found() {
|
||||
return source != null && !source.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record VpdObjectFilterDetail(
|
||||
String objectOwner,
|
||||
String objectName,
|
||||
List<Row> rows
|
||||
) {
|
||||
|
||||
public String objectDisplayName() {
|
||||
return objectOwner + "." + objectName;
|
||||
}
|
||||
|
||||
public record Row(
|
||||
VpdPolicyView policy,
|
||||
VpdFunctionSource source
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdPolicyCreateCommand(
|
||||
String objectOwner,
|
||||
String objectName,
|
||||
String policyName,
|
||||
String functionKey,
|
||||
String functionOwner,
|
||||
String functionName,
|
||||
String statementTypes,
|
||||
boolean enabled,
|
||||
boolean updateCheck,
|
||||
String filterPredicate
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdPolicyDetail(
|
||||
VpdPolicyView policy,
|
||||
String ddl
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdPolicyExplanation(
|
||||
String status,
|
||||
String modelName,
|
||||
String answer,
|
||||
String prompt,
|
||||
VpdPolicyDetail detail,
|
||||
VpdFunctionSource functionSource
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record VpdPolicyFormOptions(
|
||||
List<String> policyNames,
|
||||
List<String> schemaOwners,
|
||||
List<String> owners,
|
||||
List<VpdFunctionOption> functions,
|
||||
List<VpdPolicyTemplateOption> policyTemplates,
|
||||
List<String> statementTypes
|
||||
) {
|
||||
|
||||
public String defaultPermissionFunctionKey() {
|
||||
return functions.stream()
|
||||
.filter(VpdFunctionOption::permissionSystemDefault)
|
||||
.findFirst()
|
||||
.map(VpdFunctionOption::value)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
public String defaultPermissionFunctionKey(String objectName) {
|
||||
String preferredFunction = objectName != null
|
||||
&& objectName.toUpperCase(java.util.Locale.ROOT).startsWith("HMM_LEAVE_")
|
||||
? "HMM_LEAVE_VPD_FILTER"
|
||||
: "CB_AGENT_DOC_VPD_FILTER";
|
||||
return functions.stream()
|
||||
.filter(function -> preferredFunction.equalsIgnoreCase(function.functionName()))
|
||||
.findFirst()
|
||||
.map(VpdFunctionOption::value)
|
||||
.orElseGet(this::defaultPermissionFunctionKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdPolicyTemplateOption(
|
||||
String policyName,
|
||||
String functionOwner,
|
||||
String packageName,
|
||||
String functionName,
|
||||
String statementTypes,
|
||||
String enabled,
|
||||
String checkOption
|
||||
) {
|
||||
|
||||
public String functionKey() {
|
||||
String packagePrefix = packageName == null || packageName.isBlank() ? "" : packageName + ".";
|
||||
return functionOwner + "." + packagePrefix + functionName;
|
||||
}
|
||||
|
||||
public String filterDisplayName() {
|
||||
return functionKey();
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return policyName + " / " + filterDisplayName() + " / " + statementTypes;
|
||||
}
|
||||
|
||||
public boolean enabledValue() {
|
||||
return "YES".equalsIgnoreCase(enabled);
|
||||
}
|
||||
|
||||
public boolean updateCheckValue() {
|
||||
return "YES".equalsIgnoreCase(checkOption);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdPolicyView(
|
||||
String objectOwner,
|
||||
String objectName,
|
||||
String policyGroup,
|
||||
String policyName,
|
||||
String functionOwner,
|
||||
String packageName,
|
||||
String functionName,
|
||||
String statementTypes,
|
||||
String checkOption,
|
||||
String enabled,
|
||||
String staticPolicy,
|
||||
String policyType,
|
||||
String longPredicate
|
||||
) {
|
||||
|
||||
public String objectDisplayName() {
|
||||
return objectOwner + "." + objectName;
|
||||
}
|
||||
|
||||
public String functionDisplayName() {
|
||||
String packagePrefix = packageName == null || packageName.isBlank() ? "" : packageName + ".";
|
||||
return functionOwner + "." + packagePrefix + functionName;
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdSchemaObjectOption(
|
||||
String owner,
|
||||
String objectName,
|
||||
String objectType
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
public record VpdTargetView(
|
||||
String owner,
|
||||
String objectName,
|
||||
String objectType,
|
||||
String protectedYn,
|
||||
String ordsPath,
|
||||
String description,
|
||||
int policyCount,
|
||||
String policyNames,
|
||||
String filterNames
|
||||
) {
|
||||
|
||||
public String objectDisplayName() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public boolean protectedObject() {
|
||||
return "Y".equalsIgnoreCase(protectedYn);
|
||||
}
|
||||
|
||||
public boolean vpdApplied() {
|
||||
return policyCount > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AuditMapper {
|
||||
|
||||
void insert(AuditEvent event);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface BearerTokenMapper {
|
||||
|
||||
List<BearerTokenRecord> findAll(@Param("includeInactive") int includeInactive);
|
||||
|
||||
BearerTokenRecord findById(@Param("keyId") long keyId);
|
||||
|
||||
BearerTokenRecord findByHash(@Param("keyHash") String keyHash);
|
||||
|
||||
long nextKeyId();
|
||||
|
||||
void insertToken(BearerTokenRecord token);
|
||||
|
||||
int revokeToken(
|
||||
@Param("keyId") long keyId,
|
||||
@Param("revokedAt") LocalDateTime revokedAt,
|
||||
@Param("reason") String reason
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.AppGroup;
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.GroupCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.GroupRoleView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.GroupUserView;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface GroupMapper {
|
||||
|
||||
List<AppGroup> findAll();
|
||||
|
||||
List<GroupUserView> findGroupUsers();
|
||||
|
||||
List<GroupRoleView> findGroupRoles();
|
||||
|
||||
long nextGroupId();
|
||||
|
||||
void insertGroup(@Param("groupId") long groupId, @Param("command") GroupCreateCommand command);
|
||||
|
||||
int updateActive(@Param("groupId") long groupId, @Param("activeYn") String activeYn);
|
||||
|
||||
void insertGroupUser(@Param("groupId") long groupId, @Param("userId") long userId);
|
||||
|
||||
int deleteGroupUser(@Param("groupId") long groupId, @Param("userId") long userId);
|
||||
|
||||
void insertGroupRole(@Param("groupId") long groupId, @Param("roleId") long roleId);
|
||||
|
||||
int deleteGroupRole(@Param("groupId") long groupId, @Param("roleId") long roleId);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
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.UserMaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.service.MaskingPolicyTarget;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface MaskingRuleMapper {
|
||||
|
||||
List<MaskingRule> findAllRules();
|
||||
|
||||
List<MaskingRule> findEnabledRules();
|
||||
|
||||
MaskingRule findRuleById(@Param("ruleId") long ruleId);
|
||||
|
||||
MaskingRule findRuleByCode(@Param("ruleCode") String ruleCode);
|
||||
|
||||
long nextRuleId();
|
||||
|
||||
void insertRule(@Param("ruleId") long ruleId, @Param("command") MaskingRuleCreateCommand command);
|
||||
|
||||
int updateRuleActive(@Param("ruleId") long ruleId, @Param("enabledYn") String enabledYn);
|
||||
|
||||
List<ColumnMaskingRule> findColumnRules();
|
||||
|
||||
List<MaskingPolicyStatus> findPolicyStatuses(
|
||||
@Param("owner") String owner,
|
||||
@Param("policies") List<MaskingPolicyTarget> policies
|
||||
);
|
||||
|
||||
ColumnMaskingRule findColumnRule(@Param("columnId") long columnId);
|
||||
|
||||
void upsertColumnRule(@Param("columnId") long columnId, @Param("ruleId") long ruleId);
|
||||
|
||||
int deleteColumnRule(@Param("columnId") long columnId);
|
||||
|
||||
int deleteUserRulesForColumn(@Param("columnId") long columnId);
|
||||
|
||||
List<UserMaskingRule> findUserRules();
|
||||
|
||||
UserMaskingRule findUserRule(@Param("userId") long userId, @Param("columnId") long columnId);
|
||||
|
||||
void upsertUserRule(
|
||||
@Param("userId") long userId,
|
||||
@Param("columnId") long columnId,
|
||||
@Param("decision") String decision
|
||||
);
|
||||
|
||||
int deleteUserRule(@Param("userId") long userId, @Param("columnId") long columnId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.operation.OperationStatusRow;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface OperationStatusMapper {
|
||||
|
||||
List<OperationStatusRow> findRows();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface OrdsMetadataMapper {
|
||||
|
||||
List<OrdsHandlerView> findHandlers();
|
||||
|
||||
OrdsHandlerView findHandler(@Param("handlerId") long handlerId);
|
||||
|
||||
String findHandlerPackageSource();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface PermissionMapper {
|
||||
|
||||
List<AppRole> findRoles();
|
||||
|
||||
AppRole findRole(@Param("roleId") long roleId);
|
||||
|
||||
long nextRoleId();
|
||||
|
||||
void insertRole(@Param("roleId") long roleId,
|
||||
@Param("roleName") String roleName,
|
||||
@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 countUserRolesByRoleId(@Param("roleId") long roleId);
|
||||
|
||||
int countGroupRolesByRoleId(@Param("roleId") long roleId);
|
||||
|
||||
int countPermissionsByRoleId(@Param("roleId") long roleId);
|
||||
|
||||
List<PermissionView> findPermissionViews();
|
||||
|
||||
PermissionSet findPermissionSet(@Param("roleId") long roleId, @Param("objectId") long objectId);
|
||||
|
||||
Long findPermissionId(@Param("roleId") long roleId, @Param("objectId") long objectId);
|
||||
|
||||
Long findObjectIdByPermissionId(@Param("permissionId") long permissionId);
|
||||
|
||||
int countPermissionsByObjectId(@Param("objectId") long objectId);
|
||||
|
||||
void insertPermission(@Param("permissionId") long permissionId,
|
||||
@Param("roleId") long roleId,
|
||||
@Param("objectId") long objectId,
|
||||
@Param("action") String action,
|
||||
@Param("permissionEffect") String permissionEffect);
|
||||
|
||||
void updatePermissionAction(@Param("permissionId") long permissionId, @Param("action") String action);
|
||||
|
||||
void updatePermissionEffect(@Param("permissionId") long permissionId,
|
||||
@Param("permissionEffect") String permissionEffect);
|
||||
|
||||
void deleteRules(@Param("permissionId") long permissionId);
|
||||
|
||||
void insertRule(PermissionRule rule);
|
||||
|
||||
void deleteVisibleColumns(@Param("permissionId") long permissionId);
|
||||
|
||||
void insertVisibleColumn(@Param("permissionId") long permissionId, @Param("columnName") String columnName);
|
||||
|
||||
int deletePermission(@Param("permissionId") long permissionId);
|
||||
|
||||
long nextPermissionId();
|
||||
|
||||
long nextRuleId();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.DatabaseObjectOption;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface ProtectedObjectMapper {
|
||||
|
||||
List<ProtectedObject> findEnabled();
|
||||
|
||||
List<ProtectedObject> findEnabledWithPermissions();
|
||||
|
||||
ProtectedObject findById(@Param("objectId") long objectId);
|
||||
|
||||
ProtectedObject findByOwnerAndName(@Param("owner") String owner, @Param("objectName") String objectName);
|
||||
|
||||
List<ProtectedColumn> findColumns(@Param("objectId") long objectId);
|
||||
|
||||
List<ProtectedColumn> findColumnsByObjectIds(@Param("objectIds") List<Long> objectIds);
|
||||
|
||||
ProtectedColumn findColumnById(@Param("columnId") long columnId);
|
||||
|
||||
ProtectedColumn findColumnByObjectAndName(@Param("objectId") long objectId,
|
||||
@Param("columnName") String columnName);
|
||||
|
||||
List<DatabaseObjectOption> findDatabaseObjects();
|
||||
|
||||
List<String> findDatabaseColumns(@Param("owner") String owner, @Param("objectName") String objectName);
|
||||
|
||||
long nextObjectId();
|
||||
|
||||
long nextColumnId();
|
||||
|
||||
void insertObject(@Param("objectId") long objectId, @Param("command") ProtectedObjectCreateCommand command);
|
||||
|
||||
void insertColumn(@Param("columnId") long columnId,
|
||||
@Param("objectId") long objectId,
|
||||
@Param("columnName") String columnName,
|
||||
@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 updateDescription(@Param("objectId") long objectId, @Param("description") String description);
|
||||
|
||||
int enableObject(@Param("objectId") long objectId);
|
||||
|
||||
int disableObject(@Param("objectId") long objectId);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user