Files
jarca-ledger-mcp/docs/interface-contract.md
T
gitea-impl-agent 96e11812ce fix(dashboard): keep shell during menu transitions
Replace document navigation with native Streamlit state transitions, preserve dashboard contracts, and add regression plus authenticated browser evidence for Issue #9.
2026-10-05 00:49:28 +00:00

262 lines
13 KiB
Markdown

# Interface Contract — Dashboard 관리자 인증
## 목적과 범위
Dashboard 인증 설정, login/cleanup/logout HTTP route, signed cookie와 Streamlit gate의 외부·내부 계약을 정의한다.
- 포함 범위: 환경변수, HTTPS form POST, cookie 발급·삭제, fail-closed gate
- 제외 범위: MCP 인증, OAuth/OIDC, 다중 사용자, DB session/token registry, per-token revocation
- 사용 주체: browser, Dashboard ASGI server, Streamlit app, 운영 배포
## 계약
### auth-config — 서버 인증 설정
- 책임: 현재 configured credential과 signing secret을 server-only로 제공한다.
- 종류·프로토콜: process environment
- 호출 대상·operation: `load_auth_settings()`
- 버전: 2.0
- 호출 주체: Dashboard server
- 인증 방식: 없음
- 필요 권한: process secret read
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `JARCA_DASHBOARD_ADMIN_ID` | environment | `string` | 필수 | 없음 | non-empty | configured admin identifier | 민감 |
| `JARCA_DASHBOARD_ADMIN_PASSWORD` | environment | `string` | 필수 | 없음 | non-empty | configured credential | 민감 |
| `JARCA_DASHBOARD_SIGNING_SECRET` | environment | `string` | 필수 | 없음 | UTF-8 32 byte 이상 | signing input | 민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `AuthSettings` | server memory | `object` | 성공 시 필수 | current configured values | verifier input; never serialized | 민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `configuration_unavailable` | configuration | missing/empty/read error/short secret | 수정 후 가능 | generic unavailable | no cookie, no DB |
### login — `POST /api/auth/login`
- 책임: same-origin HTTPS credential을 검증하고 signed cookie를 발급한다.
- 종류·프로토콜: HTTPS form POST
- 호출 대상·operation: `/api/auth/login`
- 버전: 1.0
- 호출 주체: browser login form
- 인증 방식: submitted credential, server-only comparison
- 필요 권한: same-origin HTTPS
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `admin_id` | form body | `string` | 필수 | 없음 | bounded form value | submitted ID | 민감 |
| `admin_password` | form body | `string` | 필수 | 없음 | bounded form value | submitted password | 민감 |
| `remember_me` | form body | `string` | 선택 | off | `on` or absent | choose 30-day TTL | 비민감 |
| `Origin` | request header | `URL` | 필수 | 없음 | request `Host`와 scheme·hostname·effective port가 같은 HTTPS origin | CSRF origin 검증 | 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `Set-Cookie` | response header | `cookie` | success | hardened auth cookie, redacted value | persistent auth | 값은 민감 |
| `Location` | response header | `string` | success/failure | `/` or generic error path | browser navigation | 비민감 |
발급 token은 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti` claim만 가진다. `sub=dashboard-admin`은 관리자 ID가 아닌 고정 비민감 role이고, token 값과 서명은 민감하므로 browser의 hardened cookie에만 저장한다. 성공한 login 시각에 `iat`와 `exp`를 한 번 고정하며 기본 TTL은 24시간, `remember_me=on`이면 30일이다. 활동, 일반 요청, rerun 또는 인증 복원은 token을 재발급하거나 두 시각을 연장하지 않는다.
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `400` | request | malformed/oversized body | 수정 후 가능 | resubmit valid form | no cookie |
| `403` | origin | non-HTTPS or cross-origin | 수정 후 가능 | use same-origin HTTPS | no cookie |
| `invalid` | authentication | ID/password mismatch | 가능 | generic error and delay | no cookie |
| `unavailable` | configuration | invalid server configuration | config fix 후 | generic unavailable | no cookie |
### cleanup — `GET /api/auth/cleanup`
- 책임: invalid cookie를 matching scope로 삭제한 뒤 unauthenticated navigation을 완료한다.
- 종류·프로토콜: HTTPS navigation GET
- 호출 대상·operation: `/api/auth/cleanup`
- 버전: 1.0
- 호출 주체: same-origin browser/auth gate
- 인증 방식: same-origin HTTPS `Referer`; token validity 불필요
- 필요 권한: same-origin HTTPS
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `Referer` | request header | `URL` | 필수 | 없음 | request origin과 같은 HTTPS authority | navigation origin 검증 | 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `Set-Cookie` | response header | `cookie` | always | same name/path/security, `Max-Age=0` | delete stale cookie | 값은 민감 |
| `Location` | response header | `string` | success | `/` | unauthenticated page | 비민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `403` | origin | non-HTTPS/cross-origin | 가능 | retry same-origin | no DB |
### logout — `POST /api/auth/logout`
- 책임: explicit logout으로 browser cookie를 삭제한다.
- 종류·프로토콜: HTTPS form POST
- 호출 대상·operation: `/api/auth/logout`
- 버전: 1.0
- 호출 주체: authenticated browser
- 인증 방식: same-origin HTTPS; server-side revocation 없음
- 필요 권한: same-origin HTTPS
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `none` | none | `none` | 선택 | 없음 | no credential accepted | explicit logout | 없음 |
| `Origin` | request header | `URL` | 필수 | 없음 | request `Host`와 scheme·hostname·effective port가 같은 HTTPS origin | CSRF origin 검증 | 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `Set-Cookie` | response header | `cookie` | always | matching deletion, `Max-Age=0` | remove browser cookie | 값은 민감 |
| `Location` | response header | `string` | success | `/` | unauthenticated page | 비민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `403` | origin | non-HTTPS/cross-origin | 가능 | retry same-origin | no DB |
### gate — Dashboard 인증 경계
- 책임: config and cookie verification before any protected work.
- 종류·프로토콜: Python/Streamlit call
- 호출 대상·operation: `require_dashboard_authentication()`
- 버전: 2.0
- 호출 주체: `dashboard.app.main()`
- 인증 방식: current config plus signed cookie
- 필요 권한: valid token with `sub=dashboard-admin`
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `auth_cookie` | request cookie | `string` | success path | absent | current configuration으로 서명을 검증할 수 있고 claim이 정확히 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti`인 token | browser proof | 값과 서명은 민감; `sub`는 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `authenticated` | server result | `bool` | always | true/false | gate decision | 비민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `unauthenticated` | authentication | absent/invalid/expired/config failure | login 가능 | cleanup then show login | no protected work |
관리자 ID, password 또는 signing secret 변경은 server-only signing key binding을 바꾸므로 기존 token을 모두 무효화하고 cleanup으로 전이한다. DB session, token registry와 per-token revocation은 만들지 않는다.
## 호환성
- 유지할 기존 호출 방식: authenticated `page` query parameter, page dispatch, gateway와 MCP behavior
- 버전 변경 정책: REQ-001 revision 2.0; session-only boolean을 signed cookie로 대체
- 폐기 정책: client-side token storage, DB-backed session, per-token revocation 없음
- 재시도 계약: credential 실패는 최소 1초 지연; origin/request 오류는 수정 후 재시도
- 중복 처리 계약: login은 새 token 발급 가능; cleanup/logout deletion은 idempotent
- 순서 보장: config → cookie verify → cleanup if invalid → DB/page dispatch only if valid
- 만료 보장: login 성공 시 고정한 `iat`와 `exp`는 활동·일반 요청·rerun·복원에서 변경하거나 연장하지 않음
## 예시
### example-login — 정상 login
- 대상 인터페이스: `login`
#### 호출
```text
POST /api/auth/login
Origin: https://dashboard.example
admin_id=<submitted>&admin_password=<submitted>&remember_me=on
```
#### 결과
```text
303 Location: /
Set-Cookie: __Host-jarca_dashboard_auth=<redacted>; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000
```
### example-cleanup — invalid cookie cleanup
- 대상 인터페이스: `cleanup`
#### 호출
```text
GET /api/auth/cleanup
Referer: https://dashboard.example/
```
#### 결과
```text
303 Location: /
Set-Cookie: __Host-jarca_dashboard_auth=; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Path=/
```
## 검증
- 정상 계약: authentication/server tests와 loopback HTTPS evidence
- 실패 계약: wrong credential, invalid configuration, cross-origin, malformed cookie tests
- 경계 계약: expiry, rotation, oversized token, same-host different-port origin tests
- 구현·스키마·예시 정합성: `tests/test_dashboard_auth_server.py`, `tests/test_dashboard_authentication_app.py`, `tests/test_dashboard_persistent_authentication.py`, source/document redaction search
## Dashboard 메뉴 전환 계약 — REQ-002
### dashboard-menu-transition
- 책임: authenticated Dashboard에서 공통 shell/background를 유지하고 선택된 page body만 교체한다.
- 종류·프로토콜: Streamlit rerun, `st.radio` callback, query parameter
- 호출 대상·operation: `dashboard.app.main()` → `render_sidebar_nav()` → `render_dashboard_body()`
- 버전: 1.0
- 호출 주체: authenticated browser
- 인증 방식: 기존 `gate` 계약 선행; 메뉴는 인증을 우회하지 않음
- 필요 권한: valid Dashboard auth cookie
#### 상태와 입력
정확한 menu options/order는 `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`이다. selected value는 `st.session_state["dashboard_page"]`에 둔다. 직접 query의 `page`가 known option이면 해당 page를, absent/unknown이면 `요약`을 사용한다.
#### 출력·불변조건
- callback은 `st.query_params["page"]`만 갱신하고 `month`, `account_id`, `filter` 등 sibling key를 보존한다.
- menu click은 document/path navigation이 아니며 stable `st.empty` body container 안에서 target renderer를 실행한다.
- 공통 sidebar, refresh control, CSS/background는 유지하고 body만 target content로 대체한다.
- 느린 target에는 `<target> 불러오는 중…`을 표시한 뒤 renderer 완료 시 placeholder를 제거한다.
- renderer 예외는 body 안의 `페이지를 불러오지 못했습니다. 다시 시도해 주세요.`와 retry control로 대체하며 예외 상세를 노출하지 않는다.
- serialized callback이 여러 번 실행되면 마지막 selection이 query와 body의 최종 상태가 된다.
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `page_fallback` | request | missing/unknown `page` | 가능 | `요약` 표시 | sibling query 유지 |
| `renderer_unavailable` | rendering | target renderer exception | 가능 | generic error의 retry 클릭 | previous body 제거, auth/shell 유지 |
#### 검증 경계
AppTest는 callback, query mapping, synthetic renderer output과 error/loading event를 검증하지만 실제 browser의 document navigation 부재, paint timing, focus/scroll, network request 또는 perceived shell continuity를 증명하지 않는다. 따라서 `REQ-002.03-AC01`은 authenticated real-browser evidence가 필수이며 AppTest는 companion evidence일 뿐이다. history/back-forward와 path URL은 계약에서 제외한다.
상세 acceptance target은 `docs/requirements/req-002_dashboard-menu-transition.md`의 `REQ-002.01-AC01`부터 `REQ-002.09-AC01`까지를 따른다.