[Architect] #424 design VPD ORDS backoffice

Refs #424
This commit is contained in:
devmrko
2026-06-23 10:44:35 +09:00
parent 21e7e9a526
commit 5cc8026538
4 changed files with 518 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
# 설계서: Spring Boot VPD/ORDS 권한 백오피스 (#424)
> **상태**: Draft
> **작성**: [AI] Architect · **최종수정**: 2026-06-23
> **추적성** — Redmine: #424 · 관련 ADR: 없음
> · 구현 파일: `pom.xml`, `src/main/**`, `src/test/**` · 테스트: `mvn test`
## 1. 목적 (Why)
운영자와 데모 담당자가 ADB VPD/Redaction 권한 매핑, Bearer Token, ORDS 호출 결과를 한 화면에서 관리하고 검증할 수 있는 Spring Boot 백오피스를 만든다.
## 2. 범위 (Scope)
- **포함**:
- Spring Boot 3.x, Java 21 또는 17, HikariCP, MyBatis 기반 백오피스 골격
- ADB 권한 관리 테이블과 ORDS Bearer Key 검증 테이블을 조회/수정하는 서비스
- 보호 객체, 사용자, 역할, 행 규칙, 컬럼 표시 권한, Bearer Token 관리 화면
- Bearer Token을 사용해 ORDS에 `SELECT *` 성격의 조회를 실행하고 VPD/Redaction 적용 결과를 확인하는 화면
- Thymeleaf + HTMX + Alpine.js + Bootstrap 5 기반 서버 렌더링 프론트
- Audit log 저장과 오류 유형 구분 표시
- **제외 (out of scope)**:
- 외부 SSO 연동
- 운영 승인 워크플로우
- DDS DATA GRANT 직접 생성 UI
- 원격 PostgreSQL/MySQL 데이터 생성 자동화
- 운영 배포 파이프라인
## 3. 인수조건 (Acceptance Criteria)
- [ ] Spring Boot 애플리케이션이 HikariCP와 MyBatis로 ADB에 연결된다.
- [ ] 보호 객체 목록은 DB에 등록된 whitelist에서만 선택 가능하며, 자유 입력 객체명으로 SQL 또는 ORDS 호출을 만들 수 없다.
- [ ] 내부 사용자, 역할, 사용자-역할, 객체 권한, 행 규칙, 컬럼 표시 권한을 백오피스에서 조회하고 저장할 수 있다.
- [ ] Bearer Token은 발급 시 원문을 한 번만 보여주고 DB에는 hash, prefix, 만료/회수 상태만 저장한다.
- [ ] ORDS 호출 검증 화면에서 Bearer Token과 보호 객체를 선택하면 ORDS 호출 결과, 반환 컬럼, 행 수, 오류 유형을 확인할 수 있다.
- [ ] 권한 없음, 잘못된 토큰, 객체 권한 없음, VPD 0건, Redaction NULL 표시를 서로 구분해 보여준다.
- [ ] 권한 변경, 토큰 발급/회수, ORDS 검증 호출은 audit log에 남는다.
- [ ] `mvn test`로 핵심 서비스 단위 테스트와 Mapper/SQL 검증 테스트를 실행할 수 있다.
## 4. 컨텍스트 & 제약
- 의존성:
- Oracle Autonomous Database
- ORDS Handler 또는 REST Enabled SQL 성격의 조회 API
- Oracle JDBC, MyBatis, Spring Security, Thymeleaf, HTMX
- 제약:
- `.env`와 DB 비밀번호, Bearer Token 원문은 git에 저장하지 않는다.
- 객체명은 반드시 DB에 등록된 보호 객체 whitelist로 제한한다.
- 백오피스 DB 계정은 권한 관리 테이블과 검증용 ORDS 호출에 필요한 최소 권한만 갖는다.
- VPD는 행(row) 통제, Redaction은 컬럼 값 마스킹/NULL 처리로 설명하고 구현한다.
- 가정:
- 기존 SQL의 `CB_*` ORDS/VPD Bearer Key 예제를 백오피스 데이터 모델의 기준으로 삼는다.
- 첫 구현은 단일 관리자 로그인 또는 개발용 in-memory user로 시작하고, 운영 SSO는 후속 범위로 둔다.
- ORDS 호출은 백오피스 서버에서 수행하며 브라우저에는 Bearer Token 원문을 보존하지 않는다.
## 5. 아키텍처 개요
### 모듈/파일 구조
```text
pom.xml
src/main/java/com/cloudhandson/vpdbackoffice/
VpdBackofficeApplication.java
config/
DataSourceConfig.java
SecurityConfig.java
OrdsClientConfig.java
domain/
user/
permission/
token/
protectedobject/
probe/
audit/
mapper/
UserMapper.java
PermissionMapper.java
BearerTokenMapper.java
ProtectedObjectMapper.java
AuditMapper.java
service/
PermissionService.java
BearerTokenService.java
OrdsProbeService.java
AuditService.java
web/
PermissionController.java
TokenController.java
ProbeController.java
DashboardController.java
src/main/resources/
application.yml
mapper/*.xml
templates/**/*.html
static/css/app.css
static/js/app.js
src/test/java/com/cloudhandson/vpdbackoffice/
```
### 데이터 흐름
```text
관리자 브라우저
-> Spring MVC Controller
-> Service
-> MyBatis Mapper
-> ADB 권한/토큰/보호 객체 테이블
검증 호출
-> ProbeController
-> OrdsProbeService
-> BearerTokenService에서 token 상태 확인
-> ProtectedObject whitelist 확인
-> ORDS HTTP 호출 Authorization: Bearer <token>
-> 응답/오류 분류
-> Audit 저장
-> Thymeleaf fragment로 결과 영역 갱신
```
### I/O와 순수 로직 경계
- Controller는 요청 파라미터 검증과 화면 모델 구성만 담당한다.
- Service는 트랜잭션, 권한 변경, 토큰 발급/회수, ORDS 호출 오케스트레이션을 담당한다.
- Mapper는 SQL I/O만 담당한다.
- 순수 로직은 `TokenGenerator`, `TokenHasher`, `ProbeErrorClassifier`, `ObjectNamePolicy`로 분리해 단위 테스트한다.
## 6. 데이터 모델
### 백오피스 기준 테이블
| 테이블 | 역할 | 주요 컬럼 |
|---|---|---|
| `CB_APP_USER` | 내부 사용자 | `user_id`, `username`, `emp_no`, `dept_code`, `active_yn` |
| `CB_APP_ROLE` | 역할 | `role_id`, `role_name`, `description` |
| `CB_USER_ROLE` | 사용자-역할 매핑 | `user_id`, `role_id` |
| `CB_PROTECTED_OBJECT` | 보호 객체 whitelist | `object_id`, `owner`, `object_name`, `ords_path`, `enabled_yn` |
| `CB_PERMISSION` | 객체 접근 권한 | `permission_id`, `role_id`, `object_id`, `action` |
| `CB_PERMISSION_RULE` | 행 조건 | `rule_id`, `permission_id`, `rule_type`, `rule_value` |
| `CB_PROTECTED_COLUMN` | 컬럼 표시 정책 | `column_id`, `object_id`, `column_name`, `sensitive_yn`, `visible_role_id` |
| `CB_AGENT_BEARER_KEY` | Bearer Token 관리 | `key_id`, `user_id`, `key_prefix`, `key_hash`, `expires_at`, `revoked_at` |
| `CB_ORDS_PROBE_AUDIT` | ORDS 검증 이력 | `audit_id`, `key_id`, `object_id`, `status`, `row_count`, `error_code`, `created_at` |
### 경계 검증 규칙
- `owner`, `object_name`, `column_name`은 Oracle identifier 허용 문자로 검증하되, 검증 후에도 SQL 문자열 직접 조합에 사용하지 않는다.
- ORDS 검증 대상은 `CB_PROTECTED_OBJECT.enabled_yn = 'Y'`인 행만 허용한다.
- Token 원문은 발급 응답 화면에서만 사용하고 DB, log, audit에는 저장하지 않는다.
- `rule_type``ALL`, `MY_DEPT`, `SELF`, `REGION`, `CUSTOM_PREDICATE` 중 허용 값만 저장한다.
- `CUSTOM_PREDICATE`는 초기 구현에서 비활성화한다.
## 7. 함수 명세 (Function Specs)
| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? |
|------|-----------|----------------|------|------|-----------|-------|
| `issueToken` | Bearer Token을 생성하고 hash만 저장한다 | `IssuedToken issueToken(TokenIssueCommand command)` | 사용자, 만료일, 설명 | prefix와 원문 token | 비활성 사용자, 잘못된 만료일 | **복잡** |
| `revokeToken` | Bearer Token을 회수한다 | `void revokeToken(long keyId, String reason)` | key id, 사유 | 없음 | 없는 key, 이미 회수됨 | 단순 |
| `savePermissionSet` | 역할별 객체 권한과 행/컬럼 규칙을 저장한다 | `PermissionSet savePermissionSet(PermissionSetCommand command)` | 역할, 객체, 규칙 목록 | 저장된 권한 | whitelist 위반, 중복 규칙 | **복잡** |
| `runProbe` | ORDS Bearer 호출을 실행하고 결과를 분류한다 | `ProbeResult runProbe(ProbeCommand command)` | token id, object id, limit | 결과 rows, columns, status | 토큰 만료, ORDS 오류, timeout | **복잡** |
| `classifyProbeError` | ORDS/Oracle 오류를 화면 표시 상태로 분류한다 | `ProbeStatus classifyProbeError(HttpStatus status, String body)` | HTTP 상태, 응답 body | 분류 상태 | 알 수 없는 오류 | 단순 |
| `assertAllowedObject` | 보호 객체 whitelist를 검증한다 | `ProtectedObject assertAllowedObject(long objectId)` | object id | 보호 객체 | 비활성/없는 객체 | 단순 |
| `recordAudit` | 관리 작업과 검증 호출을 audit에 기록한다 | `void recordAudit(AuditEvent event)` | 이벤트 | 없음 | DB insert 실패 | 단순 |
복잡 함수별 상세 설계서는 다음 파일에 둔다.
- `fn-issueToken.md`
- `fn-savePermissionSet.md`
- `fn-runProbe.md`
## 8. 흐름 / 알고리즘
### 권한 저장
1. Controller가 역할, 보호 객체, 행 규칙, 컬럼 표시 정책 입력을 받는다.
2. Service가 role/object 존재와 활성 상태를 확인한다.
3. 행 규칙 타입과 값을 검증한다.
4. 기존 권한 세트를 transaction 안에서 갱신한다.
5. VPD 정책이 참조하는 테이블 구조에 맞춰 권한 매핑을 저장한다.
6. audit log를 남긴다.
7. HTMX fragment로 저장된 권한 요약을 반환한다.
### Token 발급
1. 관리자가 사용자와 만료일을 선택한다.
2. Service가 사용자 활성 상태와 만료일을 검증한다.
3. `vpd_live_` prefix와 충분한 난수 token body를 생성한다.
4. server-side pepper를 사용해 hash를 만든다.
5. prefix, hash, 만료일만 저장한다.
6. 원문 token은 발급 완료 화면에 한 번만 표시한다.
### ORDS 검증
1. 관리자가 token과 보호 객체를 선택한다.
2. Service가 token 상태와 보호 객체 whitelist를 확인한다.
3. ORDS URL은 DB에 등록된 `ords_path`와 서버 설정의 base URL로만 만든다.
4. `Authorization: Bearer <token>` 헤더로 ORDS를 호출한다.
5. JSON 응답이면 컬럼 목록과 행 수를 계산한다.
6. 오류 응답이면 HTTP 상태와 Oracle 오류 코드를 분류한다.
7. audit log를 남기고 화면에 결과를 표시한다.
## 9. 엣지케이스 & 에러 처리
- Bearer Token 없음: `MISSING_TOKEN`으로 표시하고 ORDS 호출하지 않는다.
- 만료/회수 token: `TOKEN_INACTIVE`로 표시하고 ORDS 호출하지 않는다.
- 잘못된 token hash: `INVALID_TOKEN` 또는 ORDS 응답의 `ORA-20002`로 표시한다.
- 보호 객체 비활성: `OBJECT_DISABLED`로 차단한다.
- 권한 매핑 없음: 호출은 성공하지만 0 rows이면 `VPD_DENY_EMPTY_RESULT`로 설명한다.
- 객체 DB 권한 없음: `ORA-00942`, `ORA-01031``OBJECT_NOT_ACCESSIBLE`로 분류한다.
- Redaction 적용: 민감 컬럼 값이 NULL이면 오류가 아니라 `MASKED_COLUMN` 표시로 보여준다.
- ORDS timeout: 기본 10초 timeout, 재시도 없음. 검증 화면에서 재실행하도록 한다.
- Audit 저장 실패: 권한 변경 작업은 rollback한다. 조회 검증 audit 실패는 오류를 반환한다.
## 10. 테스트 계획
- 단위 테스트:
- Token 생성 결과가 충분한 길이와 prefix를 갖고 hash만 저장되는지 검증
- 만료/회수 token 검증 실패
- 보호 객체 whitelist 위반 차단
- ORDS 오류 body의 `ORA-20002`, `ORA-00942`, `ORA-01031` 분류
- 권한 저장 command의 중복/빈 rule 검증
- 통합 테스트:
- MyBatis Mapper XML 로딩 테스트
- Testcontainers 사용은 Oracle 제약 때문에 초기 범위에서 제외하고 Mapper SQL은 `@MybatisTest`와 mock datasource 중심으로 검증
- 실제 ADB 연동 테스트는 `.env`가 있는 개발 환경에서 `mvn test -Pwith-adb`로 분리
- 화면 테스트:
- Controller slice test로 목록/저장/검증 fragment 렌더링 확인
- 수동 smoke test로 토큰 발급, 권한 저장, ORDS 검증 실행 확인
## 11. 리스크 & 대안 검토
- 프론트 대안:
- React/Vue SPA는 화면 표현력은 높지만 빌드 체인과 API 경계가 늘어난다.
- 이 백오피스는 서버 렌더링 폼, 테이블, 결과 fragment가 중심이므로 Thymeleaf + HTMX + Alpine.js + Bootstrap 5가 가장 단순하다.
- SQL 안전성:
- 객체명 whitelist 없이 `SELECT * FROM ${table}`을 만들면 SQL injection과 권한 우회 위험이 있다.
- 따라서 ORDS 호출 대상은 `CB_PROTECTED_OBJECT``ords_path`만 사용한다.
- Token 저장:
- 평문 저장은 운영 위험이 크다.
- hash 저장과 one-time display를 기본으로 한다.
- DDS:
- DDS UI까지 포함하면 범위가 커진다.
- 현재 요구는 VPD 형태와 ORDS Bearer 검증이 핵심이므로 DDS는 상태 조회/문서 연결만 후속 범위로 둔다.
## 12. 미해결 질문 (Open Questions)
- ORDS 검증 API는 기존 `sql/adb/22_agent_ords_security_ords_handler_setup.sql`의 Handler를 그대로 사용할지, 백오피스 전용 Handler를 추가할지 결정이 필요하다.
- 백오피스 관리자 로그인은 초기에는 local user로 둘지, 사내 인증과 연결할지 후속 결정이 필요하다.
- 컬럼 정책을 Redaction DDL까지 자동 생성할지, 관리 테이블 저장 후 DBA 적용으로 둘지 결정이 필요하다.
- 실제 구현 issue를 별도 Redmine 하위 이슈로 나눌지, #424를 Developer 단계로 계속 이동할지 결정이 필요하다.

View File

@@ -0,0 +1,86 @@
# 함수 설계서: `issueToken` (#424)
> **부모 설계서**: ./README.md · **상태**: Draft
> **작성**: [AI] Architect · **구현**: `BearerTokenService.issueToken` · **테스트**: `BearerTokenServiceTest`
## 1. 시그니처
```java
IssuedToken issueToken(TokenIssueCommand command)
```
## 2. 책임 (단일 책임, 1줄)
활성 사용자에게 새 Bearer Token을 발급하고 DB에는 원문이 아닌 hash와 prefix만 저장한다.
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|----------|------|-----------|------|
| `command.userId` | `long` | 활성 사용자여야 함 | 토큰 소유 사용자 |
| `command.expiresAt` | `OffsetDateTime` | 현재 시각 이후, 최대 정책 기간 이내 | 만료 시각 |
| `command.description` | `String` | 200자 이하 | 운영자 식별 설명 |
## 4. 출력
- **반환**: `IssuedToken`
- `keyId`: 저장된 key id
- `prefix`: 화면 식별용 prefix
- `plainToken`: 발급 직후 한 번만 보여줄 원문 token
- `expiresAt`: 만료 시각
- **부수효과**: `CB_AGENT_BEARER_KEY` insert, audit insert
## 5. 동작 / 알고리즘
1. `userId`로 사용자를 조회하고 `active_yn = 'Y'`인지 확인한다.
2. 만료일이 현재 시각 이후인지 확인한다.
3. 정책상 최대 유효 기간을 넘으면 실패한다.
4. `vpd_live_` prefix와 256bit 이상 난수 body를 생성한다.
5. 원문 token을 조합한다.
6. server-side pepper를 읽어 HMAC-SHA256 hash를 만든다.
7. `key_prefix`, `key_hash`, `expires_at`, `created_by`를 저장한다.
8. audit log를 저장한다.
9. 원문 token을 포함한 `IssuedToken`을 반환한다.
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|------|------|-----------|
| 사용자가 없음 | 발급 중단 | `UserNotFoundException` |
| 비활성 사용자 | 발급 중단 | `InactiveUserException` |
| 만료일이 과거 | 발급 중단 | `InvalidTokenExpiryException` |
| pepper 설정 없음 | 발급 중단 | `TokenConfigurationException` |
| DB 저장 실패 | transaction rollback | `DataAccessException` |
## 7. 엣지케이스
- 같은 사용자에게 여러 token 발급은 허용한다.
- prefix는 식별용이므로 중복 가능성을 낮추되, 인증 판단에는 hash만 사용한다.
- 원문 token은 log, audit, exception message에 포함하지 않는다.
## 8. 복잡도 / 성능
- 시간 복잡도는 O(1)이다.
- 호출 빈도는 낮고 운영자 작업 단위다.
- 난수 생성은 `SecureRandom` 또는 JDK 보안 API를 사용한다.
## 9. 의존성
- `UserMapper`
- `BearerTokenMapper`
- `AuditService`
- `TokenHasher`
- `Clock`
## 10. 테스트 케이스
- [ ] 정상: 활성 사용자와 미래 만료일 입력 시 원문 token과 저장 id 반환
- [ ] 정상: 저장된 값에 원문 token이 포함되지 않음
- [ ] 실패: 비활성 사용자 입력 시 예외
- [ ] 실패: 과거 만료일 입력 시 예외
- [ ] 실패: pepper 설정 없음
## 11. 추적성
- 인수조건: #424의 "Bearer Token은 발급 시 원문을 한 번만 보여주고 DB에는 hash, prefix, 만료/회수 상태만 저장한다."
- 관련 ADR: 없음

View File

@@ -0,0 +1,97 @@
# 함수 설계서: `runProbe` (#424)
> **부모 설계서**: ./README.md · **상태**: Draft
> **작성**: [AI] Architect · **구현**: `OrdsProbeService.runProbe` · **테스트**: `OrdsProbeServiceTest`
## 1. 시그니처
```java
ProbeResult runProbe(ProbeCommand command)
```
## 2. 책임 (단일 책임, 1줄)
선택된 Bearer Token과 보호 객체 whitelist를 사용해 ORDS 검증 호출을 실행하고 결과 또는 오류를 분류한다.
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|----------|------|-----------|------|
| `command.keyId` | `long` | 활성 token이어야 함 | 사용할 Bearer Token |
| `command.objectId` | `long` | 활성 보호 객체 | ORDS 호출 대상 |
| `command.limit` | `int` | 1 이상 500 이하 | 조회 행 제한 |
## 4. 출력
- **반환**: `ProbeResult`
- `status`: 성공/차단/오류 분류
- `columns`: 반환 컬럼
- `rows`: 결과 행
- `rowCount`: 행 수
- `maskedColumns`: NULL 또는 마스킹으로 판단된 컬럼
- `errorCode`, `errorMessage`: 오류 시 분류 정보
- **부수효과**: ORDS HTTP 호출, audit insert
## 5. 동작 / 알고리즘
1. `keyId`로 token 상태를 조회한다.
2. token이 만료 또는 회수 상태면 ORDS 호출 없이 실패 결과를 반환한다.
3. `objectId`로 보호 객체를 조회하고 활성 상태를 확인한다.
4. ORDS URL은 설정된 base URL과 보호 객체의 `ords_path`로 만든다.
5. 요청 timeout과 행 제한 parameter를 적용한다.
6. `Authorization: Bearer <token>` 헤더로 ORDS를 호출한다.
7. 2xx 응답이면 JSON을 파싱해 컬럼과 행 수를 계산한다.
8. 민감 컬럼이 모두 NULL인 경우 Redaction 적용 표시를 만든다.
9. 비 2xx 또는 Oracle 오류 body이면 `classifyProbeError`로 분류한다.
10. audit log를 저장한다.
11. 화면 표시용 `ProbeResult`를 반환한다.
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|------|------|-----------|
| token 없음 | ORDS 호출 안 함 | `TOKEN_NOT_FOUND` |
| token 만료/회수 | ORDS 호출 안 함 | `TOKEN_INACTIVE` |
| 보호 객체 없음/비활성 | ORDS 호출 안 함 | `OBJECT_DISABLED` |
| ORDS timeout | audit 후 오류 반환 | `ORDS_TIMEOUT` |
| `ORA-20002` | 잘못된 key로 분류 | `INVALID_TOKEN` |
| `ORA-00942` / `ORA-01031` | 객체 권한 없음으로 분류 | `OBJECT_NOT_ACCESSIBLE` |
| 2xx + 빈 rows | VPD deny 가능성 표시 | `VPD_DENY_EMPTY_RESULT` |
| 알 수 없는 오류 | 원문 일부만 표시 | `UNKNOWN_ERROR` |
## 7. 엣지케이스
- 응답이 JSON이 아니면 성공으로 보지 않고 `INVALID_ORDS_RESPONSE`로 분류한다.
- 결과가 500행을 넘지 않도록 limit 기본값과 최대값을 둔다.
- audit에는 token 원문을 저장하지 않고 `key_id`, `key_prefix`만 저장한다.
- ORDS 응답 body 전체를 audit에 저장하지 않고 오류 코드와 짧은 메시지만 저장한다.
## 8. 복잡도 / 성능
- 네트워크 I/O가 지배적이다.
- 기본 timeout은 10초다.
- 화면 검증 호출이므로 자동 재시도는 하지 않는다.
## 9. 의존성
- `BearerTokenMapper`
- `ProtectedObjectMapper`
- `RestClient` 또는 `WebClient`
- `ProbeErrorClassifier`
- `AuditService`
- `Clock`
## 10. 테스트 케이스
- [ ] 정상: 2xx JSON 응답에서 컬럼과 행 수 추출
- [ ] 정상: 빈 rows를 VPD deny 가능성으로 표시
- [ ] 정상: 민감 컬럼 NULL을 masked column으로 표시
- [ ] 실패: 만료 token이면 ORDS 호출하지 않음
- [ ] 실패: 비활성 객체이면 ORDS 호출하지 않음
- [ ] 실패: `ORA-20002` 분류
- [ ] 실패: timeout 분류
## 11. 추적성
- 인수조건: #424의 "ORDS 호출 검증 화면에서 Bearer Token과 보호 객체를 선택하면 ORDS 호출 결과, 반환 컬럼, 행 수, 오류 유형을 확인할 수 있다."
- 관련 ADR: 없음

View File

@@ -0,0 +1,88 @@
# 함수 설계서: `savePermissionSet` (#424)
> **부모 설계서**: ./README.md · **상태**: Draft
> **작성**: [AI] Architect · **구현**: `PermissionService.savePermissionSet` · **테스트**: `PermissionServiceTest`
## 1. 시그니처
```java
PermissionSet savePermissionSet(PermissionSetCommand command)
```
## 2. 책임 (단일 책임, 1줄)
역할과 보호 객체에 대한 행 규칙 및 컬럼 표시 정책을 검증한 뒤 하나의 transaction으로 저장한다.
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|----------|------|-----------|------|
| `command.roleId` | `long` | 존재하는 역할 | 권한을 받을 역할 |
| `command.objectId` | `long` | 활성 보호 객체 | 조회 대상 객체 |
| `command.action` | `String` | 초기 구현은 `SELECT`만 허용 | 작업 유형 |
| `command.rules` | `List<RuleCommand>` | 1개 이상, 타입별 값 검증 | 행 조건 |
| `command.visibleColumns` | `List<String>` | 보호 객체에 등록된 컬럼만 허용 | 표시 허용 컬럼 |
## 4. 출력
- **반환**: 저장된 `PermissionSet`
- **부수효과**: permission/rule/column mapping 갱신, audit insert
## 5. 동작 / 알고리즘
1. 역할이 존재하는지 확인한다.
2. 보호 객체가 whitelist에 있고 활성 상태인지 확인한다.
3. action이 허용 값인지 확인한다.
4. rule 목록이 비어 있으면 실패한다.
5. rule type별 rule value 형식을 검증한다.
6. 중복 rule을 제거하거나 실패 처리한다. 초기 구현은 실패 처리한다.
7. 표시 컬럼 목록이 보호 객체 컬럼 목록의 부분집합인지 확인한다.
8. transaction 안에서 기존 권한 세트를 갱신한다.
9. audit log를 저장한다.
10. 저장된 권한 세트를 다시 조회해 반환한다.
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|------|------|-----------|
| 역할 없음 | 저장 중단 | `RoleNotFoundException` |
| 보호 객체 없음/비활성 | 저장 중단 | `ProtectedObjectNotFoundException` |
| 허용되지 않은 action | 저장 중단 | `InvalidPermissionActionException` |
| rule 없음 | 저장 중단 | `InvalidPermissionRuleException` |
| rule value 형식 오류 | 저장 중단 | `InvalidPermissionRuleException` |
| 컬럼 whitelist 위반 | 저장 중단 | `InvalidColumnPolicyException` |
| audit 저장 실패 | rollback | `DataAccessException` |
## 7. 엣지케이스
- `ALL` rule과 다른 제한 rule이 같이 들어오면 충돌로 보고 실패한다.
- `SELF` rule은 대상 객체에 사용자 식별 컬럼이 등록되어 있어야 허용한다.
- `MY_DEPT` rule은 대상 객체에 부서 컬럼이 등록되어 있어야 허용한다.
- 컬럼 목록이 빈 값이면 민감 컬럼 표시 없음으로 처리한다.
## 8. 복잡도 / 성능
- rule/column 검증은 입력 목록 크기에 대해 O(n)이다.
- 운영자 저장 작업 단위이므로 대량 처리 성능은 주요 병목이 아니다.
## 9. 의존성
- `RoleMapper`
- `ProtectedObjectMapper`
- `PermissionMapper`
- `AuditService`
- `ObjectNamePolicy`
## 10. 테스트 케이스
- [ ] 정상: SELECT 권한과 REGION rule 저장
- [ ] 정상: 민감 컬럼 제외 정책 저장
- [ ] 실패: `ALL``REGION` rule 동시 입력
- [ ] 실패: 비활성 보호 객체 입력
- [ ] 실패: 등록되지 않은 컬럼 입력
- [ ] 실패: audit 저장 실패 시 permission 저장 rollback
## 11. 추적성
- 인수조건: #424의 "내부 사용자, 역할, 사용자-역할, 객체 권한, 행 규칙, 컬럼 표시 권한을 백오피스에서 조회하고 저장할 수 있다."
- 관련 ADR: 없음