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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@
**Vulnerability:** The document hashing routine in `DefaultDocumentConversionService` processed file streams without enforcing any maximum size limit on the bytes read. An attacker could exploit this by uploading a maliciously large stream (or exploiting a compression bomb if unzipping), exhausting system memory, CPU, or disk space (DoS).
**Learning:** Checking the declared file size (e.g., `file.getSize()`) in initial validation is not always sufficient if the input stream itself can be spoofed or dynamically expanded during reading. The actual bytes read must be verified against bounds continuously.
**Prevention:** Always enforce a strict, configurable size limit (e.g., `ConversionProperties.maxUploadSizeBytes`) within the `while` loop that reads from untrusted input streams. Track `totalRead` and throw an exception immediately if the limit is exceeded.
## 2026-08-13 - [CRITICAL] Admin API 인증 및 권한 검증 누락 수정
**Vulnerability:** `AdminController`의 관리자 전용 API(전체 작업 조회, 작업 삭제, 데드레터 재시도 등)에 대해 사용자 인증 및 권한(tenant:configure) 검증이 누락되어 누구나 관리자 기능을 호출할 수 있었습니다.
**Learning:** 새로운 컨트롤러 추가 시 기존의 `TenantAccessService`를 통한 AOP/필터 기반 공통 처리가 아닌, 각 메서드 내에서 명시적인 권한 요구(tenantAccessService.require) 방식을 사용하고 있어 휴먼 에러로 검증 코드가 쉽게 누락될 수 있었습니다.
**Prevention:** 모든 엔드포인트는 추가 시 반드시 권한 검증 로직이 포함되었는지 확인하고, 보안 문서에 정의된 권한(`docs/security/2026-07-02-auth-tenant-model.md`의 `tenant_admin` 권한)과 일치하는 상수(예: `TENANT_CONFIGURE`)를 적용해야 합니다.
140 changes: 10 additions & 130 deletions docs/deployment/clearfolio-buyer-connector.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ info:
description: >
Import seed for a buyer-owned gateway or Power Platform custom connector.
This spec reflects the current Clearfolio Viewer JSON API contract and the
signed tenant-header scaffold. It is not a production OIDC/JWT profile.
buyer-demo signed-header scaffold. It is not a production OIDC/JWT profile.
license:
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
name: Proprietary - buyer diligence use only
url: https://github.com/ContextualWisdomLab/clearfolio
servers:
- url: https://{gatewayHost}
description: Buyer gateway in front of Clearfolio Viewer.
Expand All @@ -19,15 +19,13 @@ security:
- clearfolioTenantHeaders: []
tags:
- name: Conversion
description: Document intake, status polling, deletion, and retry operations.
description: Document intake, status polling, and retry operations.
- name: Viewer
description: Viewer bootstrap operations that return signed artifact URLs.
- name: ArtifactLinks
description: Signed artifact-link creation, revocation, byte delivery, and read evidence.
description: Signed artifact-link creation, revocation, and read evidence.
- name: Analytics
description: Tenant-scoped KPI snapshot and export evidence operations.
- name: Operations
description: Operational endpoints intentionally exposed without tenant identity.
paths:
/api/v1/convert/jobs:
post:
Expand Down Expand Up @@ -100,28 +98,6 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
delete:
tags:
- Conversion
operationId: deleteConversionJob
summary: Delete a conversion job and generated artifacts.
description: Requires `job:delete`; missing and cross-tenant jobs are hidden.
parameters:
- $ref: "#/components/parameters/JobId"
- $ref: "#/components/parameters/TenantId"
- $ref: "#/components/parameters/SubjectId"
- $ref: "#/components/parameters/Permissions"
- $ref: "#/components/parameters/ClaimsIssuedAt"
- $ref: "#/components/parameters/ClaimsSignature"
responses:
"204":
description: Conversion job and generated artifacts deleted.
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/api/v1/convert/jobs/{jobId}/retry:
post:
tags:
Expand Down Expand Up @@ -223,7 +199,7 @@ paths:
tags:
- ArtifactLinks
operationId: revokeArtifactLink
summary: Revoke a previously issued signed artifact link.
summary: Revoke a previously issued artifact link.
description: Requires `artifact-link:revoke`; intended for operators.
parameters:
- $ref: "#/components/parameters/TokenId"
Expand Down Expand Up @@ -278,57 +254,6 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
/artifacts/{docId}.pdf:
get:
tags:
- ArtifactLinks
operationId: getPdfArtifact
summary: Read a converted PDF through a signed artifact token.
description: >
Serves bytes only for a succeeded conversion with an issued, unexpired,
unrevoked, document/tenant/checksum-bound artifact token. The token may
be supplied either as the `artifactToken` query parameter or as a Bearer
credential. At most one `bytes` range is accepted and verified reads are
recorded by the server.
security:
- artifactTokenQuery: []
- artifactTokenBearer: []
parameters:
- $ref: "#/components/parameters/DocId"
- $ref: "#/components/parameters/Range"
responses:
"200":
description: Full PDF artifact.
content:
application/pdf:
schema:
type: string
format: binary
"206":
description: Requested single byte range of the PDF artifact.
headers:
Content-Range:
description: Returned byte range and complete PDF length as `bytes start-end/total`.
schema:
type: string
content:
application/pdf:
schema:
type: string
format: binary
"401":
description: Signed artifact token is missing, malformed, expired, or otherwise unauthorized.
"403":
description: Signed artifact token is revoked or otherwise forbidden.
"404":
description: Conversion job or PDF artifact is absent or unavailable.
"416":
description: Range is malformed or unsatisfiable for the current PDF bytes.
headers:
Content-Range:
description: Complete PDF length as `bytes */total`.
schema:
type: string
/api/v1/analytics/kpi-snapshot:
get:
tags:
Expand Down Expand Up @@ -379,24 +304,6 @@ paths:
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
/healthz:
get:
tags:
- Operations
operationId: getHealthz
summary: Check process liveness.
description: >
Returns only the static process-liveness payload shipped on protected main.
This endpoint intentionally requires no tenant identity and does not claim
dependency or traffic readiness.
security: []
responses:
"200":
description: Process is alive.
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
components:
securitySchemes:
clearfolioTenantHeaders:
Expand All @@ -407,31 +314,21 @@ components:
Buyer gateway supplies the full signed Clearfolio tenant header set.
The signature alone is not enough without tenant, subject, permissions,
and issued-at headers.
artifactTokenQuery:
type: apiKey
in: query
name: artifactToken
description: Short-lived Clearfolio artifact token issued for this document.
artifactTokenBearer:
type: http
scheme: bearer
bearerFormat: ClearfolioArtifactToken
description: Short-lived Clearfolio artifact token supplied as an Authorization bearer credential.
parameters:
TenantId:
name: X-Clearfolio-Tenant-Id
in: header
required: true
schema:
type: string
example: tenant-example
example: buyer-demo
SubjectId:
name: X-Clearfolio-Subject-Id
in: header
required: true
schema:
type: string
example: operator-example
example: buyer-demo-operator
Permissions:
name: X-Clearfolio-Permissions
in: header
Expand All @@ -443,7 +340,7 @@ components:
The gateway must sign this canonical form, not the raw string.
schema:
type: string
example: job:create,job:read,job:delete,viewer:read,artifact-link:create,analytics:read
example: job:create,job:read,viewer:read,artifact-link:create,analytics:read
ClaimsIssuedAt:
name: X-Clearfolio-Claims-Issued-At
in: header
Expand Down Expand Up @@ -505,13 +402,6 @@ components:
required: true
schema:
type: string
Range:
name: Range
in: header
required: false
description: Optional single HTTP byte range, for example `bytes=0-1023`.
schema:
type: string
responses:
BadRequest:
description: Bad request or validation failure.
Expand Down Expand Up @@ -544,16 +434,6 @@ components:
schema:
$ref: "#/components/schemas/ApiErrorResponse"
schemas:
HealthResponse:
type: object
required:
- status
additionalProperties: false
properties:
status:
type: string
enum:
- ok
SubmitConversionResponse:
type: object
required:
Expand Down Expand Up @@ -790,4 +670,4 @@ components:
type: string
details:
type: object
additionalProperties: true
additionalProperties: true
39 changes: 0 additions & 39 deletions scripts/test_buyer_openapi_license_contract.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public final class TenantPermissions {
*/
public static final String ANALYTICS_READ = "analytics:read";

/**
* Permission required to configure tenant or access admin endpoints.
*/
public static final String TENANT_CONFIGURE = "tenant:configure";

private TenantPermissions() {
}
}
Loading
Loading