Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
### Security
- 정책 재정의 승인자의 원문 식별자를 감사 로그에서 제거하고, 전용 회전형 키와 도메인 분리를 사용하는 HMAC 기반 `approverFingerprint`로 대체했습니다. 전용 키가 없으면 원문이나 비키 해시로 폴백하지 않고 비상관 `unavailable` 표식을 기록합니다.
- 감사 가명화 키의 소유권, 회전, 보존, 사고 대응 및 GDPR상 가명정보의 개인정보 지위를 문서화하고, 원문 승인자 식별자와 승인 토큰이 로그에 남지 않는 회귀 테스트를 추가했습니다.
- 관리자 API에 서명된 tenant claim 검증, `admin:read`/`admin:write` 최소 권한, tenant 소유권 검사를 적용했습니다. 누락 및 cross-tenant 객체는 동일한 not-found 응답으로 은폐합니다.
- 관리자 delete/retry가 검증된 `TenantContext`를 tenant-aware service mutation boundary에 전달하도록 변경해, controller 우회 호출에서도 소유권 검사가 적용되고 read-then-mutate 간격이 생기지 않도록 했습니다.
- 관리자 허용·거부·미존재·재시도 불가·실패 결정을 actor/tenant별 도메인 분리 HMAC 지문으로 기록하고, raw subject·tenant·claim signature·문서 메타데이터가 감사 로그와 retry provenance에 남지 않도록 했습니다.
- buyer-demo profile의 tenant-claims HMAC secret 환경변수 직접 바인딩을 제거하고 공통 Spring config-tree secret mount에서 읽도록 변경했습니다.

# Changelog

Expand Down Expand Up @@ -54,4 +58,4 @@
- 저장소 보안 정책, Maven/GitHub Actions Dependabot 설정, 기본 CodeQL/중앙 SAST 운영 지침, 다운로드 파일명 정규화 Jazzer fuzz target을 추가해 Scorecard 보안 거버넌스 신호를 보강했습니다.

### Fixed
- 뷰어 UI의 재시도 버튼 로딩 상태가 내부 DOM을 손상시키지 않고 안전하게 복원되도록 수정
- 뷰어 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
71 changes: 71 additions & 0 deletions docs/security/2026-08-05-administrative-authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Tenant-scoped administrative authorization

## Decision

Clearfolio's administrative job endpoints are not global superuser APIs. They are tenant-scoped operations that evaluate signed subject claims, an explicit administrative permission, the requested operation, and the target job's tenant ownership on every request.

The implementation follows deny-by-default and least-privilege principles. Listing requires `admin:read`; deletion and dead-letter retry require `admin:write`. Possessing an opaque UUID is never sufficient authorization. Missing and cross-tenant jobs intentionally return the same not-found response so an object identifier cannot be used to enumerate another tenant's documents or operational state.

Administrative endpoints never fall back to unsigned demo-header mode. If the tenant-claims HMAC verifier is absent or its configured key contains fewer than 32 UTF-8 bytes, the endpoints return `503 Service Unavailable` before repository access. This makes a missing or weak trust anchor an observable deployment failure rather than an authorization bypass.

## Trust boundary

The `X-Clearfolio-*` claim headers are an internal adapter contract between Clearfolio and an authenticated gateway or host such as naruon. They are not public client credentials. The upstream gateway must authenticate the caller, construct canonical tenant, subject, permission, and issue-time claims, and sign them with the tenant-claims HMAC key.

Clearfolio verifies the signature and freshness before evaluating permissions. Deployments must strip untrusted inbound copies of these headers before adding verified claims. The service remains standalone because the claim verifier is an injectable component, but production must not expose the internal header adapter directly to arbitrary clients.

The tenant-claims HMAC secret is read from the shared Spring config-tree secret mount as `clearfolio.tenant-claims.hmac-secret`. The buyer-demo profile no longer maps a secret-bearing environment variable directly into runtime configuration. Environment variables may select non-secret operational values or bootstrap a mounted credential store, but runtime authentication reads the mounted property. The mounted tenant-claims key must contain at least 32 UTF-8 bytes for privileged administrative endpoints.

## Authorization sequence

Every endpoint applies the same fail-closed sequence:

1. Confirm that a strong signed-claim verifier is configured; otherwise return `503` before service access.
2. Parse the tenant, subject, permissions, issue time, and claim signature.
3. Verify signed claims and their freshness.
4. Require the action-specific permission.
5. Pass the verified `TenantContext` into the object-specific service mutation.
6. Select the target through a tenant-scoped repository lookup before evaluating or applying the mutation.
7. Return a non-enumerating not-found response for absent or cross-tenant objects.
8. Emit privacy-safe authorization evidence for the resulting outcome.

List responses filter the complete repository result to the verified tenant before applying the optional dead-letter status filter. Delete and retry do not perform controller-level read-then-write authorization. Their service contracts receive the verified tenant context and enforce ownership at the mutation boundary, so non-HTTP callers cannot reach an unscoped administrative mutation by bypassing the controller.

The legacy two-argument retry method remains only as a compatibility contract for non-administrative adapters. Administrative HTTP flows call the tenant-aware three-argument method. The durable implementation performs the tenant-scoped selection before invoking the shared state transition and therefore does not rely on a controller-side ownership check.

## Audit evidence

Administrative evidence contains only:

- a controlled action code;
- a controlled outcome code;
- HTTP status;
- tenant and actor HMAC fingerprints in separate domains;
- an opaque job UUID when applicable;
- a numeric result count for list operations.

It does not contain raw tenant identifiers, raw subject identifiers, claim signatures, permission headers, filenames, job messages, document text, or artifact bytes. The retry provenance stored with a job uses the actor-domain fingerprint rather than the source subject identifier. Pseudonymized values remain personal data and inherit the retention, access, rotation, and incident-response requirements in `2026-08-04-audit-pseudonymization.md`.

## Verification requirements

Automated tests must exercise the real signed-claim verifier and prove:

- absent and weak verifier keys make privileged endpoints unavailable before service access;
- missing, malformed, expired, and incorrectly signed claims fail before service access;
- missing `admin:read` or `admin:write` permissions fail before service access;
- list results contain only tenant-owned jobs for all dead-letter filter states;
- missing and cross-tenant delete/retry targets produce indistinguishable not-found responses;
- delete and retry cross a tenant-aware service boundary without a separate controller lookup or unscoped administrative mutation call;
- the durable retry service rejects null, missing, and cross-tenant contexts without state transition or worker enqueue;
- accepted retry provenance is a domain-separated keyed fingerprint, never a raw or unkeyed subject value;
- not-found, not-eligible, repository failure, deletion failure, and retry failure paths return stable non-leaking responses;
- audit output contains no raw tenant, subject, signature, filename, or document data;
- JaCoCo reports 100% line and branch coverage for the `com.clearfolio.viewer.*` production package.

## References

Hu, V. C., Ferraiolo, D., Kuhn, D. R., Schnitzer, A., Sandlin, K., Miller, R., & Scarfone, K. (2014). *Guide to attribute based access control (ABAC) definition and considerations* (NIST Special Publication 800-162, updated August 2, 2019). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-162

OWASP Foundation. (2023). *API1:2023 broken object level authorization*. OWASP API Security Top 10. https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/

OWASP Foundation. (n.d.). *Authorization cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 5, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
Loading