2단계 PortalConsole 연계 API 계약
1. 목적과 책임 경계
2단계에서는 브라우저가 FileStore를 직접 호출하지 않는다. PortalConsole는 사용자·조직 정보의 원본(Source of Truth)을 제공하고, 각 소비 포탈은 이를 동기화해 보유한 뒤 포탈 자체 콘텐츠의 첨부파일 접근 권한을 결정한다. 소비 포탈 서버의 Portal FileStore Adapter가 권한 결정 결과를 포함해 FileStore를 직접 서버 간 호출한다.
PortalConsole는 사용자·조직 정보와 portalId, fileStoreEnabled, fileStoreConsumerId를 관리한다. 소비 포탈은 동기화된 사용자·조직 정보를 바탕으로 문서·프로세스·업무 객체별 첨부파일 권한을 관리한다. FileStore는 저장 backend, 저장 prefix, MIME·용량 제한, read/write 정책, retention, legal hold, lifecycle, R2/S3 자격증명과 저장 감사 로그를 관리한다. 콘텐츠 권한 ACL과 저장 정책의 세부값은 PortalConsole에 복제하지 않는다.
PortalConsole ── 사용자·조직 원본
↓ sync
소비 포탈 ── 콘텐츠별 첨부파일 ACL 결정
↓ 권한 결과를 포함한 서버 요청
소비 포탈 Portal FileStore Adapter
↓ service token
FileStore ── 저장 정책과 lifecycle
↓
R2/S3/local storagePortalConsole는 문서·프로세스·업무 객체의 세부 ACL을 판정하지 않는다. FileStore도 콘텐츠 ACL을 재판정하지 않으며, 포탈의 권한 결과를 감사 추적용으로만 기록한다. FileStore는 자체 저장 정책과 lifecycle 규칙을 언제나 최종 적용한다.
2. 인증과 공통 헤더
브라우저에는 FileStore 서비스 토큰을 노출하지 않는다. 소비 포탈의 Portal FileStore Adapter만 FileStore service token을 보유하며, PortalConsole와 소비 포탈 간 사용자·조직 동기화 인증은 별도로 관리한다.
소비 포탈의 Portal FileStore Adapter가 FileStore를 호출할 때 모든 요청에 다음 헤더를 포함한다.
Authorization: Bearer <FILE_STORE_SERVICE_TOKEN>
Content-Type: application/json
X-FileStore-Consumer: <fileStoreConsumerId>
X-Request-Id: <requestId>FileStore는 토큰으로 서버 호출을 인증하고, X-FileStore-Consumer와 요청 본문의 consumerId가 일치하는지 검증한다. X-Request-Id와 포탈의 권한 결과 요약은 FileStore 감사 이벤트 detail에 기록한다. 서비스 토큰, R2 access key, secret key, 콘텐츠 ACL 원문은 브라우저 응답이나 감사 로그에 기록하지 않는다.
3. PortalConsole와 포탈 설정
PortalConsole는 사용자·조직 원본과 포탈별 FileStore 연결 정보를 관리한다.
{
"portalId": "document-library",
"fileStoreEnabled": true,
"fileStoreConsumerId": "document-library"
}소비 포탈은 PortalConsole에서 동기화한 사용자·조직 정보와 포탈 자체 콘텐츠 ACL을 관리한다. 포탈 ACL에는 최소한 다음과 같은 권한 판단이 가능해야 한다.
{
"actorSubject": "user-123",
"operation": "read",
"resourceType": "document",
"resourceId": "document-123",
"allowed": true,
"authorizationIssuer": "document-library",
"authorizedAt": "2026-09-06T00:00:00Z",
"authorizationExpiresAt": "2026-09-06T00:05:00Z"
}fileStoreConsumerId가 없거나 fileStoreEnabled=false이면 소비 포탈 Adapter는 FileStore를 호출하지 않고 file_store_disabled를 반환한다. mappingId, backendId, Bucket, Endpoint, secretRef, 파일 크기와 MIME 정책은 PortalConsole나 소비 포탈에 저장하지 않는다.
3.1 관리자 콘솔용 소비 포털 목록 (FileStore → PortalConsole)
FileStore 관리자 콘솔이 consumerId를 손으로 입력하면 오타가 첫 업로드에서야 드러난다. 그래서 원본인 PortalConsole에서 목록을 읽어 선택하게 한다.
GET /api/internal/file-store/v1/portals
Authorization: Bearer <FILE_STORE_SYNC_TOKEN>{
"data": {
"portals": [
{ "portalId": "document-library", "label": "문서 라이브러리", "consumerId": "document-library", "fileStoreEnabled": true },
{ "portalId": "process-center", "label": "ProcessCenter", "consumerId": "process-center", "fileStoreEnabled": false }
]
},
"meta": { "source": "file-store-sync", "secretReturned": false }
}- R2 자격증명 전달(
POST /api/internal/file-store/v1/portal-r2-credentials)과 같은 토큰을 쓴다. - 기본은 활성 포털 전체(
enabled=true)이며, 각 항목은file_store_enabled를 함께 담는다. 아직 FileStore를 켜지 않은 포털도 내려준다 — 저장 정책을 먼저 만들고 그 정책 ID를 포털에 등록하는 셋업 순서 때문이다. ?fileStoreOnly=1이면 FileStore를 켠 포털만,?includeDisabled=1이면 비활성 포털까지.file_store_consumer_id가 비어 있으면portal_id로 대신한다.- 응답에는 access key·secret key·service token 원문이 포함되지 않는다.
4. 소비 포탈 Portal FileStore Adapter API
4.1 업로드 세션
POST /api/<consumer-portal>/v1/attachments/upload-session
Authorization: Bearer <portal-server-auth>
X-Request-Id: <requestId>요청 예시:
{
"portalId": "document-library",
"ownerType": "document",
"ownerId": "document-123",
"fileName": "manual.pdf",
"contentType": "application/pdf",
"classification": "INTERNAL",
"organizationIds": ["dept_58016148e5504c1c98deb7182d02e224"],
"sourceVersion": "v1",
"sourceMenuId": "documents",
"sourceEntityType": "document",
"sourceEntityId": "document-123",
"authorization": {
"operation": "write",
"resourceType": "document",
"resourceId": "document-123",
"allowed": true,
"issuer": "document-library",
"expiresAt": "2026-09-06T00:05:00Z"
}
}소비 포탈 서버가 자체 콘텐츠 ACL을 확인한 뒤 자신의 Adapter가 fileStoreEnabled, fileStoreConsumerId, 포탈 인증과 authorization assertion의 발급자·operation·만료를 확인하고 FileStore를 호출한다. Adapter는 콘텐츠 ACL의 세부 규칙을 다시 판정하지 않는다.
FileStore 호출:
POST /api/file-store/v1/assets/upload-session
Authorization: Bearer <FILE_STORE_SERVICE_TOKEN>
X-FileStore-Consumer: document-library
X-Request-Id: <requestId>FileStore 호출 본문에는 consumerId, FileStore 정책의 mappingId, actorSubject, requestId, 권한 결과의 요약값이 포함된다. mappingId는 PortalConsole 설정값이 아니라 FileStore 정책 조회 결과 또는 안전한 서버 설정에서 선택한다.
응답:
{
"data": {
"assetId": "asset-uuid",
"status": "PENDING_SCAN",
"uploadUrl": "https://storage-provider/presigned-url",
"expiresInSeconds": 900,
"requestId": "request-uuid"
}
}4.2 업로드 완료
POST /api/directory/v1/attachments/:assetId/complete
Authorization: Bearer <portal-server-auth>
X-Request-Id: <requestId>포탈 서버는 동일한 콘텐츠 ACL과 업로드 세션 맥락을 확인한 뒤 자신의 Adapter를 통해 FileStore에 bytes, checksum, actorSubject, requestId를 전달한다. 동일한 assetId, 동일한 checksum·bytes로 반복 호출하면 기존 결과를 반환한다. 값이 다르면 idempotency_conflict를 반환한다.
4.3 다운로드 URL
POST /api/directory/v1/attachments/:assetId/read-url
Authorization: Bearer <portal-server-auth>
X-Request-Id: <requestId>소비 포탈 서버가 파일에 대한 콘텐츠 read 권한을 확인한 뒤 자신의 Adapter를 통해 FileStore의 read-url API를 호출한다. PortalConsole와 포탈은 저장소 credential이나 영구 locator를 저장하지 않는다.
4.4 삭제 (출처 단위)
삭제의 트리거는 소비 포탈의 출처(게시물/노드) 수명주기다. FileStore 는 삭제를 결정하지 않는다 — 포탈이 콘텐츠 삭제 권한을 확인한 뒤, 그 출처에 묶인 파일을 출처 단위로 하드 삭제한다.
POST /api/file-store/v1/assets/delete-by-source
Authorization: Bearer <FILE_STORE_SERVICE_TOKEN>
X-FileStore-Consumer: document-library
X-Request-Id: <requestId>{
"consumerId": "document-library",
"sourceEntityType": "document",
"sourceEntityId": "document-123",
"actorSubject": "directory-admin"
}응답은 { "data": { "deleted": 3, "assetIds": ["…"] } } 다. FileStore 는 consumerId+출처에 묶인 파일의 레코드와 저장 객체를 즉시 삭제하고 파일별 DELETED 감사 이벤트를 남긴다. 보존·법적보존(soft delete 후 cleanup) 개념은 두지 않으며, 같은 조건으로 다시 호출해도 매칭이 없으면 deleted: 0(멱등)을 돌려준다.
5. 오류 계약
소비 포탈 Adapter는 FileStore의 내부 오류와 storage provider 오류를 브라우저에 그대로 노출하지 않는다.
| HTTP | code | 의미 |
|---|---|---|
| 400 | invalid_attachment_request | 필드, authorization assertion, checksum, bytes 형식 오류 |
| 401 | service_auth_required | 서버 간 인증 실패 |
| 403 | attachment_forbidden | 소비 포탈의 콘텐츠 ACL 또는 포탈 연결 권한 부족 |
| 404 | attachment_not_found | 파일 또는 포탈 설정 없음 |
| 409 | idempotency_conflict | 동일 요청 키에 다른 payload 사용 |
| 409 | file_store_disabled | 포탈에서 첨부파일 기능 비활성 |
| 413 | attachment_too_large | FileStore 최종 용량 제한 초과 |
| 415 | attachment_type_not_allowed | FileStore 최종 MIME 제한 초과 |
| 429 | attachment_retryable | 일시적 제한, Retry-After 포함 |
| 502/503 | file_store_unavailable | 소비 포탈 Adapter에서 재시도 가능한 FileStore 장애 |
6. requestId와 감사 추적
requestId는 소비 포탈 또는 Adapter가 최초 생성하고 포탈 응답까지 유지한다. FileStore audit event의 detail.requestId에는 다음 값을 기록한다.
{
"requestId": "request-uuid",
"actorSubject": "user-or-service-subject",
"portalId": "document-library",
"operation": "write",
"authorizationIssuer": "document-library",
"authorizationAllowed": true
}FileStore는 원래 actorSubject와 requestId를 모두 보존한다. 로그와 감사 이벤트에는 access token 원문, presigned URL query string, 파일 내용, 콘텐츠 ACL 원문과 secret을 기록하지 않는다.
7. 재시도·만료·멱등성
- upload-session 재시도는 동일
Idempotency-Key가 있으면 동일한 세션 결과를 반환하거나 기존 세션 상태를 반환한다. - complete 재시도는 동일 checksum·bytes이면 성공 결과를 반환한다.
- authorization assertion이 만료되면 포탈은 자체 ACL을 다시 확인한 뒤 새 assertion을 발급한다.
- presigned URL이 만료되면 소비 포탈 Adapter는 새 upload-session을 발급하며 기존 파일을 임의로 ACTIVE 처리하지 않는다.
- FileStore가 5xx 또는 네트워크 오류를 반환하면 소비 포탈 Adapter는 제한된 지수 백오프와 최대 재시도 횟수를 적용한다.
- R2 업로드 후 complete가 실패하면 PENDING_SCAN 파일을 즉시 hard delete하지 않고 retention/cleanup 정책에 따라 정리한다.
8. 단계별 완료 기준
- PortalConsole가 사용자·조직 원본과 포탈의 FileStore 연결 정보를 제공한다.
- 소비 포탈이 동기화된 사용자·조직 정보를 바탕으로 콘텐츠 ACL을 판정한다.
- 소비 포탈의 Portal FileStore Adapter만 FileStore service token을 보유한다.
- 포탈 ACL에서 권한 없는 요청이 FileStore에 도달하기 전에 거부된다.
- upload-session, complete, read-url, delete 중계 흐름이
requestId하나로 추적된다. - 동일 complete 재시도가 멱등적으로 처리된다.
- authorization assertion과 presigned URL 만료, FileStore 일시 오류가 안전하게 처리된다.
- 통합 테스트에서 성공, 포탈 ACL 거부, consumerId 위조, 정책 거부, 만료, 재시도, 감사 로그를 확인한다.
9. 구현 전제
현재 FileStore 저장소에는 PortalConsole와 소비 포탈 구현체가 포함되어 있지 않다. 다음 구현은 해당 서비스 저장소에서 진행해야 한다.
- PortalConsole의 사용자·조직 원본 API와 동기화 계약
- 포탈별
fileStoreEnabled와fileStoreConsumerId연결 - 소비 포탈의 콘텐츠 ACL과 authorization assertion 생성
- 소비 포탈의 Portal FileStore Adapter와 FileStore client
- FileStore service token secret 연동
- 기존 포탈의 직접 FileStore 호출 제거 또는 compatibility facade 전환
FileStore 저장소는 X-Request-Id 헤더 또는 요청 본문의 metadata로 requestId, authorizationIssuer, authorizationAllowed, authorizationOperation을 받아 lifecycle 감사 이벤트 detail에 기록한다. 서비스 토큰과 ACL 원문은 기록하지 않는다. PortalConsole·소비 포탈 저장소가 연결되면 이 metadata를 사용하는 end-to-end 통합 테스트를 수행한다.