Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4f96b69
fix: rebuild tenant-scoped administrator authorization on current parent
seonghobae Aug 5, 2026
9c66efc
docs: align connector secret configuration
seonghobae Aug 5, 2026
7b7feef
docs: document config-tree tenant claim secret
seonghobae Aug 5, 2026
f4ae8dd
docs: align agent secret guidance
seonghobae Aug 5, 2026
ea57a47
test: require tenant-scoped administrative listing
seonghobae Aug 5, 2026
885bc85
test: define tenant-scoped repository contract
seonghobae Aug 5, 2026
b87f28e
test: define tenant-scoped service listing contract
seonghobae Aug 5, 2026
42463f9
feat: add fail-closed tenant-scoped job query
seonghobae Aug 5, 2026
9eb342f
feat: enforce tenant predicate before listing jobs
seonghobae Aug 5, 2026
1689e09
feat: add fail-closed tenant-scoped service listing
seonghobae Aug 5, 2026
7e3c0b7
feat: delegate administrative lists to tenant query
seonghobae Aug 5, 2026
c335862
fix(security): enforce tenant scope before admin list materialization
seonghobae Aug 5, 2026
d355782
test(security): require tenant-aware defaults to fail closed
seonghobae Aug 5, 2026
8fbf2d1
fix(security): fail closed for unscoped adapter mutations
seonghobae Aug 5, 2026
e29b39f
docs(security): define fail-closed adapter mutation defaults
seonghobae Aug 5, 2026
5bf134e
docs(changelog): record fail-closed tenant mutation defaults
seonghobae Aug 5, 2026
8d717dc
test(security): align retry contract with fail-closed defaults
seonghobae Aug 5, 2026
5409117
test(security): require atomic tenant-scoped mutations
seonghobae Aug 5, 2026
274eed4
feat(security): add atomic tenant-scoped delete contract
seonghobae Aug 5, 2026
cf68487
feat(security): add atomic tenant-scoped retry contract
seonghobae Aug 5, 2026
c63d3bc
fix(security): make in-memory tenant mutations atomic
seonghobae Aug 5, 2026
e5ea572
fix(security): route admin mutations through atomic tenant scope
seonghobae Aug 5, 2026
62571c3
test(security): cover atomic in-memory tenant mutations
seonghobae Aug 5, 2026
650e5de
test(security): cover fail-closed repository adapter default
seonghobae Aug 5, 2026
3292630
docs(security): specify atomic tenant mutation evidence
seonghobae Aug 5, 2026
774f684
docs(changelog): record atomic tenant mutation boundary
seonghobae Aug 5, 2026
1440b8b
test(security): reject stale tenant index and mutation observations
seonghobae Aug 5, 2026
5a557c5
fix(security): invalidate stale tenant content indexes
seonghobae Aug 5, 2026
a1949a9
fix(security): make stale index recovery collision-safe
seonghobae Aug 5, 2026
59107cf
test(security): cover fail-closed job identifier collisions
seonghobae Aug 5, 2026
0abea27
refactor(security): keep job identifier collisions fail closed
seonghobae Aug 5, 2026
cafb891
docs(security): define tenant-bound secondary index invariant
seonghobae Aug 5, 2026
d4eb774
docs(changelog): record stale tenant index defenses
seonghobae Aug 5, 2026
201e85e
refactor(coverage): preserve stale-index validation without unreachab…
seonghobae Aug 5, 2026
b47464f
test: expose tenant index replacement race
seonghobae Aug 5, 2026
e059824
fix: keep tenant content index atomic during replacement
seonghobae Aug 5, 2026
e3d9079
docs: record atomic tenant index updates
seonghobae Aug 5, 2026
310b2ee
docs: define atomic tenant index invariant
seonghobae Aug 5, 2026
d6103aa
test(concurrency): require deterministic replacement lock contention
seonghobae Aug 5, 2026
6f522f1
test(security): require missing scoped job IDs to fail closed
seonghobae Aug 5, 2026
4161ad0
ci(test): prove missing scoped job ID regression
seonghobae Aug 5, 2026
03f1ee6
ci(test): run scoped missing-ID RED proof after reopen
seonghobae Aug 5, 2026
3db2377
fix(security): conceal missing scoped lookup identifiers
seonghobae Aug 5, 2026
60518a8
fix(security): fail closed on missing scoped mutation IDs
seonghobae Aug 5, 2026
17d9d66
chore(ci): remove one-shot missing-ID RED proof
seonghobae Aug 5, 2026
3e519b2
docs(security): define missing scoped identifier behavior
seonghobae Aug 5, 2026
7737503
docs(changelog): record missing scoped ID concealment
seonghobae Aug 5, 2026
1fc6da2
test(security): require scoped lookup adapters to fail closed
seonghobae Aug 5, 2026
5dfbb9f
fix(security): fail closed in scoped repository adapter default
seonghobae Aug 5, 2026
aef159b
test(security): require concrete tenant-scoped identifier lookup
seonghobae Aug 5, 2026
2ed7cf9
fix(security): implement scoped identifier lookup in adapter
seonghobae Aug 5, 2026
8dcf76f
test(security): require all scoped lookup defaults to fail closed
seonghobae Aug 5, 2026
808e57d
fix(security): make content-hash scoped adapter default fail closed
seonghobae Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,18 @@ Codex, Cursor, opencode, …) working in this repo.
- Reference implementation: xtrmLLMBatchPython's pgcrypto-encrypted Postgres
credential registry (`get_credential(name)`). Reuse that pattern (a DB-backed
KV is fine) unless a dedicated KV is adopted.
- **This repo applies** — it is a Spring Boot service with real runtime secrets
(artifact-token HMAC secret, tenant-claims HMAC secret). **Known deviation to
migrate:** those secrets are currently injected straight from env via Spring
placeholders in `application-buyer-demo.yml`
(`clearfolio.artifact-token.secret: ${CLEARFOLIO_ARTIFACT_TOKEN_SECRET:}`,
`clearfolio.tenant-claims.hmac-secret: ${CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET:}`,
consumed by `ArtifactLinkService` / `TenantAccessService`). Move these to a
KV-backed lookup so env is only the bootstrap transport into the KV. New
secrets/credentials must go through the KV from the start, not new env reads.
- **This repo applies** — it is a Spring Boot service with real runtime secrets.
The tenant-claims HMAC secret is loaded as
`clearfolio.tenant-claims.hmac-secret` from the Spring config-tree credential
mount selected by the non-secret `CLEARFOLIO_SECRET_CONFIG_DIR` bootstrap
setting; do not restore direct runtime environment binding for that key.
**Known deviation to migrate:** the artifact-token HMAC secret is still
injected directly from an environment placeholder in
`application-buyer-demo.yml`
(`clearfolio.artifact-token.secret: ${CLEARFOLIO_ARTIFACT_TOKEN_SECRET:}`),
consumed by `ArtifactLinkService`. Move it to a KV-backed lookup so env is
only the bootstrap transport into the KV. New secrets/credentials must go
through the KV from the start, not new env reads.

### Code exploration

Expand Down
46 changes: 31 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
# Changelog

## [Unreleased]

### Added

- **UI UX 개선**: 'Details' 버튼 클릭 시, 작업 상세 정보 로드 중에 사용자가 명시적인 로딩 상태를 확인할 수 있도록 'Loading...' 텍스트와 비활성화 상태를 표시하도록 추가했습니다.
- **관리자용 단건 작업 삭제 및 재시도 API 추가**
- 특정 변환 작업을 삭제할 수 있는 `DELETE /api/v1/admin/convert/jobs/{jobId}` 엔드포인트를 추가했습니다.
- 실패(dead-lettered) 상태인 작업을 관리자가 재시도 큐에 등록할 수 있는 `POST /api/v1/admin/convert/jobs/{jobId}/retry` 엔드포인트를 추가했습니다.
- **비동기 버튼 로딩 피드백 및 상태 복원 개선**
- KPI 스냅샷 증거를 다시 불러오는 `refreshKpiEvidence` 동작 중에 "Refresh evidence" 버튼을 비활성화하고 "Refreshing..."이라는 피드백을 제공하여 사용자의 중복 클릭을 방지했습니다.
- 버튼 상태 변경 시 내부 DOM 구조를 보존하기 위해 `Array.from(button.childNodes)`로 원래 노드를 저장하고, 성공 및 실패 후 `finally` 블록에서 `replaceChildren(...)`으로 안전하게 복원하도록 구현했습니다.

### Changed

- PDF.js WebJar를 `6.1.200`으로 올리고, Clearfolio가 동일 버전의 `pdf.mjs`와 `pdf.worker.mjs`를 직접 사용해 서명된 same-origin artifact의 첫 페이지를 렌더링하도록 통합했습니다. 패키징·셸 경로·서명된 `artifactToken` 흐름을 회귀 테스트로 고정했습니다.
- CI가 pull request의 정확한 head SHA를 명시적으로 체크아웃하고 검증하며, 합성 merge revision은 별도 호환성 작업에서 검증하도록 분리했습니다.
- Maven `verify` 단계에서 JaCoCo production line 및 branch missed count가 각각 0인지 강제하고, 실패 시 누락 위치 진단을 출력하도록 했습니다.
- Jazzer fuzzing도 pull request의 정확한 head SHA를 명시적으로 체크아웃하고 검증하도록 강화했습니다.

### Security

- 정책 재정의 승인자의 원문 식별자를 감사 로그에서 제거하고, 전용 회전형 키와 도메인 분리를 사용하는 HMAC 기반 `approverFingerprint`로 대체했습니다. 전용 키가 없으면 원문이나 비키 해시로 폴백하지 않고 비상관 `unavailable` 표식을 기록합니다.
- 감사 가명화 키의 소유권, 회전, 보존, 사고 대응 및 GDPR상 가명정보의 개인정보 지위를 문서화하고, 원문 승인자 식별자와 승인 토큰이 로그에 남지 않는 회귀 테스트를 추가했습니다.
- 경로·쿼리 파라미터 타입 변환 실패 응답에서 사용자가 제출한 거부 값을 고정된 `[redacted]` 표식으로 대체해 오류 응답을 통한 개인정보·비밀값 반사를 차단했습니다. 값이 실제로 없었던 경우에만 `null` 진단을 유지합니다.
- 관리자 API에 서명된 tenant claim 검증, `admin:read`/`admin:write` 최소 권한, tenant 소유권 검사를 적용했습니다. 누락 및 cross-tenant 객체는 동일한 not-found 응답으로 은폐합니다.
- 관리자 목록 조회는 repository 경계에서 tenant predicate를 적용한 뒤 선택적 dead-letter 필터를 처리하도록 변경했습니다.
- 관리자 delete/retry가 검증된 `TenantContext`를 tenant-aware service mutation boundary에 전달하도록 변경해 controller 우회 호출에서도 소유권 검사가 적용되도록 했습니다.
- tenant-aware service 기본 구현은 global lookup 또는 legacy mutation을 호출하지 않고 `false`/`NOT_FOUND`로 실패 종료합니다. 모듈형 대체 adapter는 원자적 tenant-scoped mutation을 명시적으로 구현해야 관리자 작업을 성공시킬 수 있습니다.
- 관리자 delete/retry는 `deleteByTenantAndId`와 `retryDeadLetteredForTenant` 원자적 tenant-scoped persistence contract를 사용합니다. artifact cleanup은 소유권이 확인된 repository 삭제가 성공한 뒤에만 실행되며, worker enqueue는 소유권 확인과 dead-letter 상태 전이가 원자적으로 성공한 뒤에만 실행됩니다.
- tenant-scoped lookup/delete/retry는 누락된 job UUID를 예외로 노출하지 않고 empty/`false`/`NOT_FOUND`로 실패 종료하며 기존 저장 상태를 변경하지 않습니다.
- tenant-and-content-hash 보조 인덱스가 현재 UUID 레코드의 tenant/hash와 일치하는지 재검증하고, 동일 UUID 교체 시 이전 인덱스를 제거하며, find-or-store UUID 충돌을 fail-closed 처리해 stale 인덱스나 stale 관찰이 다른 tenant 작업으로 해석되는 경로를 차단했습니다.
- tenant-scoped 삭제와 동일 UUID 교체가 동시에 발생해도 primary job map과 tenant-content secondary index가 하나의 임계 구역에서 함께 갱신되도록 직렬화해, 교체 작업의 유효 인덱스를 지연된 삭제 정리가 제거하는 경쟁 조건을 차단했습니다.
- 관리자 허용·거부·미존재·재시도 불가·실패 결정을 actor/tenant별 도메인 분리 HMAC 지문으로 기록하고, raw subject·tenant·claim signature·문서 메타데이터가 감사 로그와 retry provenance에 남지 않도록 했습니다.
- buyer-demo profile의 tenant-claims HMAC secret 환경변수 직접 바인딩을 제거하고 공통 Spring config-tree secret mount에서 읽도록 변경했습니다.

# Changelog

## [Unreleased]

### 추가된 기능 (Added)
- **관리자용 단건 작업 삭제 및 재시도 API 추가**
- 특정 변환 작업을 삭제할 수 있는 `DELETE /api/v1/admin/convert/jobs/{jobId}` 엔드포인트를 추가했습니다.
- 실패(dead-lettered) 상태인 작업을 관리자가 재시도 큐에 등록할 수 있는 `POST /api/v1/admin/convert/jobs/{jobId}/retry` 엔드포인트를 추가했습니다.

- **비동기 버튼 로딩 피드백 및 상태 복원 개선**
- KPI 스냅샷 증거를 다시 불러오는 `refreshKpiEvidence` 동작 중에 "Refresh evidence" 버튼을 비활성화하고 "Refreshing..." 이라는 피드백을 제공하여 사용자의 중복 클릭을 방지했습니다.
- 버튼 상태 변경 시 내부 DOM 구조를 보존하기 위해 `Array.from(button.childNodes)`로 원래 노드를 저장하고, 성공 및 실패 후 `finally` 블록에서 `replaceChildren(...)`으로 안전하게 복원하도록 구현했습니다.
### Fixed

- 뷰어 UI의 재시도 버튼 로딩 상태가 내부 DOM을 손상시키지 않고 안전하게 복원되도록 수정했습니다.

## [0.1.0] - 2026-06-25

### 추가된 기능 (Added)

- **비동기 버튼 로딩 상태 UX 개선 (Async Button Loading States)**
- 문서 제출(`submitDocument`), 데모 데이터 로드(`loadDemoData`), 실패 작업 재시도(`retryActiveJob`) 등 비동기 요청을 수행하는 버튼들에 대해 처리 중 명시적인 로딩 상태(Loading, Submitting, Retrying 등)를 추가했습니다.
- 사용자의 중복 클릭을 방지하기 위해 작업 중에는 버튼이 비활성화되도록 수정했습니다.
Expand All @@ -41,9 +58,11 @@
- 관련 `AdminJobListResponse` DTO 모델과 이를 처리하는 Repository 및 Service 계층의 `findAll`/`getAllJobs` 메서드를 추가했습니다.

### 테스트 커버리지 (Tests)

- 신규 구현된 Repository, Service, Controller 계층에 대한 유닛 테스트(Unit Tests)를 작성하여 JaCoCo 기준 라인 및 브랜치 커버리지 100%를 달성했습니다.

### 보안 (Security)

- **의존성 취약점 일괄 정리 (trivy-fs / osv-scan 대응)**: Spring Boot 부모 POM을 `3.5.0`에서 `3.5.16`으로 올려 Spring Framework, Netty, Reactor Netty, logback 관련 다수의 HIGH/MEDIUM 권고를 해소했습니다.
- Jackson 계열을 `jackson-bom` import로 `2.22.1`에 고정하여 jackson-databind case-insensitive deserialization bypass 권고(GHSA-5jmj-h7xm-6q6v / CVE-2026-54515)를 제거했습니다.
- Apache Tika 표준 파서를 통해 유입되던 전이 의존성을 `dependencyManagement`로 고정했습니다: junrar `7.6.0`(경로 순회 RCE/파일 쓰기), commons-io `2.20.0`(XmlStreamReader DoS), commons-lang3 `3.18.0`, BouncyCastle `bcprov-jdk18on 1.84` 및 `bcpkix-jdk18on 1.84`(CRITICAL/Medium). 전체 347개 테스트 통과를 확인했습니다.
Expand All @@ -52,6 +71,3 @@
- 루트 `LICENSE`와 Maven license metadata를 추가해 Scorecard License alert가 표준 Apache-2.0 파일을 확인할 수 있게 했습니다.
- logback-core 신규 권고(GHSA-jhq6-gfmj-v8fx) 대응을 위해 Logback 관리 버전을 `1.5.35`로 고정했습니다.
- 저장소 보안 정책, Maven/GitHub Actions Dependabot 설정, 기본 CodeQL/중앙 SAST 운영 지침, 다운로드 파일명 정규화 Jazzer fuzz target을 추가해 Scorecard 보안 거버넌스 신호를 보강했습니다.

### Fixed
- 뷰어 UI의 재시도 버튼 로딩 상태가 내부 DOM을 손상시키지 않고 안전하게 복원되도록 수정
53 changes: 38 additions & 15 deletions docs/deployment/2026-07-02-buyer-deployment-integration-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,23 @@ The deployment cannot yet prove:

## Runtime Profile

Use the `buyer-demo` Spring profile for a buyer sandbox:
Use the `buyer-demo` Spring profile for a buyer sandbox. Runtime key material is
loaded from a Spring Boot config-tree mount. `CLEARFOLIO_SECRET_CONFIG_DIR`
selects that mount and is not itself secret. The mounted
`clearfolio.tenant-claims.hmac-secret` file must contain at least 32 UTF-8 bytes
and must be provisioned through the deployment platform's secret manager in
shared environments.

For a local sandbox, create an owner-readable config-tree file before startup:

```bash
mkdir -p .clearfolio/buyer-demo
umask 077
mkdir -p .clearfolio/buyer-demo/secrets
openssl rand -base64 48 \
> .clearfolio/buyer-demo/secrets/clearfolio.tenant-claims.hmac-secret

export SPRING_PROFILES_ACTIVE=buyer-demo
export CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET="replace-with-gateway-shared-secret"
export CLEARFOLIO_SECRET_CONFIG_DIR="$PWD/.clearfolio/buyer-demo/secrets/"
export CLEARFOLIO_ARTIFACT_TOKEN_SECRET="replace-with-artifact-token-secret"
export CLEARFOLIO_ARTIFACT_LINK_LEDGER_PATH="$PWD/.clearfolio/buyer-demo/artifact-link-ledger.log"
export CLEARFOLIO_ANALYTICS_SNAPSHOT_LEDGER_PATH="$PWD/.clearfolio/buyer-demo/kpi-snapshot-ledger.log"
Expand All @@ -49,17 +59,21 @@ mvn spring-boot:run
```

The profile file is
`src/main/resources/application-buyer-demo.yml`. It uses environment variables
only; no secret value is committed.
`src/main/resources/application-buyer-demo.yml`. Non-secret operational settings
may use environment variables. Tenant-claims HMAC key material is not bound from
a runtime secret environment variable; it is read as
`clearfolio.tenant-claims.hmac-secret` from the shared config-tree import in
`application.yml`. `CLEARFOLIO_TENANT_CLAIMS_MAX_SKEW_SECONDS` remains a
non-secret runtime setting.

For a Power Platform embedding test, replace `CLEARFOLIO_FRAME_ANCESTORS` with
the exact buyer allowlist after the gateway hostname is known. Keep it narrow;
do not use a wildcard until a security owner explicitly accepts that risk.

## Gateway Claim Contract

When `CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET` is set, every protected JSON API
call must include:
When the mounted `clearfolio.tenant-claims.hmac-secret` property is present,
every protected JSON API call must include:

- `X-Clearfolio-Tenant-Id`
- `X-Clearfolio-Subject-Id`
Expand Down Expand Up @@ -96,6 +110,11 @@ gateway must send **and sign** already-canonical values: e.g.
`viewer:read,job:read`. Sign what the verifier will re-derive, not the raw
string.

The authenticated gateway must remove all untrusted inbound
`X-Clearfolio-*` claim headers before it maps the authenticated principal,
constructs canonical claims, signs them, and forwards the replacement header
set. Browsers and external API clients are not trusted claim issuers.

Buyer-demo permission set:

```text
Expand All @@ -106,17 +125,20 @@ Production role mapping should later replace this scaffold with validated
gateway or OIDC claims. Do not hand-roll JWT parsing in this service.

For any environment that sets `SPRING_PROFILES_ACTIVE=production`, the service
fails startup unless `CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET` is present. The
buyer-demo profile can still run unsigned for local screenshots, but production
cannot accidentally inherit that unsigned mode.
fails startup unless the config-tree mount supplies a sufficiently strong
`clearfolio.tenant-claims.hmac-secret`. Setting only
`CLEARFOLIO_SECRET_CONFIG_DIR` without the required secret file does not enable
signed claims. The buyer-demo profile can still run unsigned for local
screenshots, but production cannot accidentally inherit that unsigned mode.

## Integration Flow

1. Buyer browser, Power Platform, or internal workflow authenticates at the
buyer-controlled gateway.
2. Gateway maps the principal to Clearfolio tenant id, subject id, and
permissions.
3. Gateway signs the Clearfolio headers and forwards requests to
2. Gateway strips untrusted inbound Clearfolio claim headers, maps the principal
to Clearfolio tenant id, subject id, and permissions, and canonicalizes the
mapped values.
3. Gateway signs the canonical Clearfolio headers and forwards requests to
`POST /api/v1/convert/jobs`, status, viewer bootstrap, retry, artifact-link,
and analytics APIs.
4. Clearfolio verifies the signed headers, enforces permissions, and hides
Expand Down Expand Up @@ -240,8 +262,9 @@ The buyer sandbox should not be promoted to production until these gates close:
- buyer-release license-policy evidence remains green with
`--require-no-review`, attribution drift check remains green, and final legal
release review is obtained;
- `SPRING_PROFILES_ACTIVE=production` starts only with configured signed tenant
claims and later replaces the scaffold with validated OIDC/JWT claims;
- `SPRING_PROFILES_ACTIVE=production` starts only when the config-tree mount
contains a strong `clearfolio.tenant-claims.hmac-secret`, and later replaces
the scaffold with validated OIDC/JWT claims;
- validated gateway or OIDC JWT issuer, audience, expiry, key rotation, and role
mapping;
- durable conversion job repository with persisted state transitions;
Expand Down
7 changes: 5 additions & 2 deletions docs/deployment/clearfolio-buyer-connector.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ paths:
operationId: submitConversionJob
summary: Submit a document for asynchronous preview conversion.
description: >
Requires `job:create`. The buyer gateway must add signed Clearfolio
tenant headers when `CLEARFOLIO_TENANT_CLAIMS_HMAC_SECRET` is enabled.
Requires `job:create`. The authenticated buyer gateway must strip any
untrusted inbound `X-Clearfolio-*` claim headers, then add canonical
signed Clearfolio tenant headers. Runtime verification reads
`clearfolio.tenant-claims.hmac-secret` from the config-tree mount
selected by `CLEARFOLIO_SECRET_CONFIG_DIR`.
parameters:
- $ref: "#/components/parameters/TenantId"
- $ref: "#/components/parameters/SubjectId"
Expand Down
Loading