feat(dashboard): persist administrator login
This commit is contained in:
+71
-122
@@ -1,154 +1,103 @@
|
||||
# Dashboard 관리자 인증 인터페이스 계약
|
||||
# Interface Contract — Dashboard 관리자 인증
|
||||
|
||||
## 목적과 범위
|
||||
|
||||
Streamlit Dashboard의 관리자 credential 입력, 환경 설정 판정, browser session 인증과 로그아웃 호출 규칙을 정의한다.
|
||||
Dashboard의 인증 설정, server-side login/logout HTTP endpoint, signed cookie와 Streamlit 인증 경계 계약을 정의한다.
|
||||
|
||||
- 포함 범위: Dashboard 환경변수 credential, 로그인·로그아웃 UI, 인증 결과와 오류, 기존 page dispatch 경계
|
||||
- 제외 범위: MCP 인증, 다중 사용자, 영속 session, OIDC·SSO, TLS와 Ingress
|
||||
- 사용 주체: Dashboard 사용자, 배포 운영자, `dashboard/app.py`, Dashboard page view
|
||||
- 포함: 관리자 credential, signing secret, login/logout POST, token/cookie, fail-closed page gate
|
||||
- 제외: MCP 인증, OAuth/OIDC, 다중 사용자, token registry, 외부 인증 middleware
|
||||
- 공개 origin은 HTTPS여야 한다.
|
||||
|
||||
## 계약
|
||||
## 인터페이스 목록
|
||||
|
||||
### dashboard-auth-config — 관리자 인증 설정
|
||||
|
||||
- 책임: 배포 secret에서 단일 관리자 credential을 fail-closed로 읽는다.
|
||||
- 종류·프로토콜: process environment
|
||||
- 호출 대상·operation: `dashboard.authentication.load_admin_credentials`
|
||||
- 버전: 1.0
|
||||
- 호출 주체: Dashboard 인증 경계
|
||||
- 인증 방식: 해당 없음; 인증을 구성하는 입력
|
||||
- 필요 권한: Dashboard process environment를 설정할 배포 권한
|
||||
|
||||
#### 입력
|
||||
|
||||
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `JARCA_DASHBOARD_ADMIN_ID` | 환경변수 | `string` | 필수 | 없음 | 비어 있지 않은 문자열 | 단일 관리자 ID | 예 |
|
||||
| `JARCA_DASHBOARD_ADMIN_PASSWORD` | 환경변수 | `string` | 필수 | 없음 | 비어 있지 않은 평문 password secret | 관리자 password | 예 |
|
||||
|
||||
#### 출력
|
||||
|
||||
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `AdminCredentials` | process memory | `dataclass` | 성공 시 필수 | 두 비어 있지 않은 문자열 | 현재 rerun에서만 사용할 검증 값 | 예 |
|
||||
|
||||
#### 오류
|
||||
|
||||
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
||||
| 인터페이스 | 종류 | 호출 주체 | 제공 주체 | 버전 | 상태 |
|
||||
|---|---|---|---|---|---|
|
||||
| `AUTH_CONFIGURATION_ERROR` | 설정 | 누락, 빈 값, 비문자열 또는 environment read 실패 | 설정 수정 후 가능 | 일반 설정 오류만 표시하고 Dashboard 중단 | 기존 인증 boolean 폐기, 금융 상태 무변경 |
|
||||
| `dashboard-auth-config` | environment | 배포 시스템 | `dashboard.authentication` | 2.0 | active |
|
||||
| `POST /api/auth/login` | HTTPS | browser form | `dashboard.auth_server` | 1.0 | active |
|
||||
| `POST /api/auth/logout` | HTTPS | browser form | `dashboard.auth_server` | 1.0 | active |
|
||||
| `dashboard-auth-cookie` | Secure cookie | browser/Streamlit handshake | `dashboard.auth_server` | 1.0 | active |
|
||||
| `dashboard-auth-gate` | Python/Streamlit | `dashboard.app` | `dashboard.authentication` | 2.0 | active |
|
||||
|
||||
### dashboard-login — 관리자 로그인
|
||||
## 상세 계약
|
||||
|
||||
- 책임: 제출 credential을 검증하고 현재 browser session의 인증 상태를 설정한다.
|
||||
- 종류·프로토콜: Streamlit form rerun
|
||||
- 호출 대상·operation: `dashboard.authentication.require_dashboard_authentication`
|
||||
- 버전: 1.0
|
||||
- 호출 주체: 미인증 Dashboard 사용자
|
||||
- 인증 방식: 관리자 ID·password의 constant-time 비교
|
||||
- 필요 권한: 없음
|
||||
### dashboard-auth-config
|
||||
|
||||
#### 입력
|
||||
| 이름 | 타입 | 필수 | 제약 | 민감정보 |
|
||||
|---|---|---|---|---|
|
||||
| `JARCA_DASHBOARD_ADMIN_ID` | string | 예 | non-empty | 예 |
|
||||
| `JARCA_DASHBOARD_ADMIN_PASSWORD` | string | 예 | non-empty | 예 |
|
||||
| `JARCA_DASHBOARD_SIGNING_SECRET` | string | 예 | UTF-8 32 byte 이상 | 예 |
|
||||
|
||||
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `admin_id` | Streamlit text input | `string` | 필수 | 빈 문자열 | 사용자 제출 문자열 | 관리자 ID 후보 | 예 |
|
||||
| `password` | Streamlit password input | `string` | 필수 | 빈 문자열 | 사용자 제출 문자열 | 관리자 password 후보 | 예 |
|
||||
| `page` | URL query parameter | `string` | 선택 | `요약` | 기존 Dashboard page 이름 | 인증 성공 후 이동할 page | 아니요 |
|
||||
누락·빈 값·비문자열·read error·짧은 signing secret은 일반 설정 오류로 fail-closed한다. Secret 원문이나 read-error 원인은 사용자 응답과 log에 포함하지 않는다.
|
||||
|
||||
#### 출력
|
||||
### POST /api/auth/login
|
||||
|
||||
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `_jarca_dashboard_authenticated` | `st.session_state` | `boolean` | 성공 후 필수 | `true` | 현재 session의 인증 여부 | 아니요 |
|
||||
| 화면 | Streamlit UI | `view` | 필수 | 로그인 또는 인증된 Dashboard | 현재 인증 상태의 사용자 화면 | 금융 화면은 인증 후 민감 |
|
||||
| 인증 event | application log | `string` | 제출 후 필수 | event와 correlation ID | 운영 진단 신호 | 아니요 |
|
||||
- Content-Type: `application/x-www-form-urlencoded`
|
||||
- Transport: trusted ASGI scope의 request scheme이 `https`여야 함
|
||||
- Origin: request host·effective port와 같은 HTTPS origin만 허용
|
||||
- 최대 body: 4096 bytes
|
||||
- 입력:
|
||||
|
||||
#### 오류
|
||||
| 필드 | 타입 | 필수 | 의미 |
|
||||
|---|---|---|---|
|
||||
| `admin_id` | string | 예 | 제출 관리자 ID |
|
||||
| `admin_password` | string | 예 | 제출 password |
|
||||
| `remember_me` | string | 아니요 | 값이 `on`이면 30일, 아니면 24시간 |
|
||||
|
||||
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
||||
|---|---|---|---|---|---|
|
||||
| `INVALID_CREDENTIALS` | 인증 | ID 또는 password 불일치 | 가능 | 최소 1초 후 동일 일반 오류 표시 | 금융 상태 무변경, credential 입력 제거 |
|
||||
| `AUTH_CONFIGURATION_ERROR` | 설정 | credential source 무효 | 설정 수정 후 가능 | 일반 설정 오류만 표시 | 인증 상태 폐기, 금융 상태 무변경 |
|
||||
- 성공: `303 Location: /`, hardened cookie 1개 발급
|
||||
- credential 실패: 최소 1초 후 `303 Location: /?auth_error=invalid`, cookie 없음
|
||||
- 설정 실패: `303 Location: /?auth_error=unavailable`, cookie 없음
|
||||
- origin 실패: `403`, cookie 없음
|
||||
- plain HTTP 또는 신뢰되지 않은 forwarded scheme: `403`, cookie 없음
|
||||
- malformed/oversized request: `400`, cookie 없음
|
||||
- ID와 password는 모두 `hmac.compare_digest`로 비교한다.
|
||||
|
||||
### dashboard-logout — 관리자 로그아웃
|
||||
### dashboard-auth-cookie
|
||||
|
||||
- 책임: 현재 browser session의 인증 상태를 즉시 폐기한다.
|
||||
- 종류·프로토콜: Streamlit button rerun
|
||||
- 호출 대상·operation: sidebar `로그아웃`
|
||||
- 버전: 1.0
|
||||
- 호출 주체: 인증된 Dashboard 사용자
|
||||
- 인증 방식: 현재 session의 인증 boolean
|
||||
- 필요 권한: 인증된 session
|
||||
| 속성 | 값 |
|
||||
|---|---|
|
||||
| 이름 | `__Host-jarca_dashboard_auth` |
|
||||
| `Secure` | true |
|
||||
| `HttpOnly` | true |
|
||||
| `SameSite` | `Lax` |
|
||||
| `Path` | `/` |
|
||||
| `Domain` | 없음 |
|
||||
| `Max-Age` | 86400 또는 2592000 |
|
||||
|
||||
#### 입력
|
||||
Token payload는 `v`, `sub`, `iat`, `exp`, `jti`만 허용한다. 서명 key는 signing secret과 현재 admin password를 domain-separated HMAC-SHA256으로 결합해 파생하며 `itsdangerous.URLSafeSerializer` SHA-256 signer를 사용한다. Token은 일반 요청에서 갱신하지 않는다.
|
||||
|
||||
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `로그아웃` | Streamlit button | `click event` | 필수 | 없음 | 단일 click | 인증 종료 요청 | 아니요 |
|
||||
### POST /api/auth/logout
|
||||
|
||||
#### 출력
|
||||
- Same-origin HTTPS POST만 허용한다.
|
||||
- 성공: `303 Location: /`과 동일 name/path/security 속성의 `Max-Age=0` cookie deletion
|
||||
- 금융 상태와 DB는 변경하지 않는다.
|
||||
- Stateless token 복사본은 개별 폐기하지 않는다. 강제 무효화는 password/signing-secret rotation으로 수행한다.
|
||||
|
||||
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 인증 상태 | `st.session_state` | `absent` | 필수 | key 제거 | 현재 session 재인증 요구 | 아니요 |
|
||||
| 화면 | Streamlit UI | `view` | 필수 | 로그인 화면 | 로그아웃 완료 | 아니요 |
|
||||
### dashboard-auth-gate
|
||||
|
||||
#### 오류
|
||||
`dashboard.app.main()`은 page config 직후 gate를 호출한다. Gate는 현재 설정으로 cookie signature와 strict claims를 검증한다. 유효할 때만 DB path, gateway와 page dispatch를 실행한다. Token, credential, signing material은 `st.session_state`에 저장하지 않는다. 비민감 correlation ID만 session state에 둘 수 있다.
|
||||
|
||||
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|
||||
|---|---|---|---|---|---|
|
||||
| `없음` | 없음 | 없음 | 해당 없음 | 해당 없음 | 인증 boolean 제거 외 금융 상태 무변경 |
|
||||
## 호환성·순서·재시도
|
||||
|
||||
## 호환성
|
||||
|
||||
- 유지할 기존 호출 방식: 인증 성공 후 기존 `page` query parameter와 Dashboard page dispatch를 유지한다.
|
||||
- 버전 변경 정책: 환경변수 이름 또는 session 계약 변경은 requirement revision과 본 문서를 함께 변경한다.
|
||||
- 폐기 정책: 두 환경변수는 대체 계약 승인 전까지 폐기하지 않는다.
|
||||
- 재시도 계약: 설정 오류는 설정 수정 후 rerun, credential 오류는 각 제출마다 독립 재시도한다.
|
||||
- 중복 처리 계약: 반복 실패는 잠금을 만들지 않으며 각 실패에 최소 1초 지연을 적용한다. 로그아웃 재시도는 금융 상태를 변경하지 않는다.
|
||||
- 순서 보장: 설정 판정 → session 판정 → 로그인 또는 로그아웃 → 인증 성공 후에만 DB 경로 취득과 page dispatch 순서를 유지한다.
|
||||
- 기존 `page` query parameter와 page dispatch는 유효 인증 이후 그대로 유지한다.
|
||||
- REQ-001 revision 2.0이 revision 1.0의 session-only boolean 계약을 signed-cookie 계약으로 대체한다.
|
||||
- 설정 판정 → cookie 검증 → DB path → page dispatch 순서를 유지한다.
|
||||
- password, signing secret 또는 관리자 ID 변경은 기존 token을 거부한다.
|
||||
- 반복 실패는 application lock을 만들지 않으며 각 credential 실패에 최소 1초 지연을 적용한다.
|
||||
|
||||
## 예시
|
||||
|
||||
### config-example — 배포 환경변수 이름
|
||||
|
||||
- 대상 인터페이스: `dashboard-auth-config`
|
||||
|
||||
#### 호출
|
||||
|
||||
```text
|
||||
JARCA_DASHBOARD_ADMIN_ID=<secret-injected-admin-id>
|
||||
JARCA_DASHBOARD_ADMIN_PASSWORD=<secret-injected-password>
|
||||
```
|
||||
|
||||
#### 결과
|
||||
|
||||
```text
|
||||
유효한 두 값: 로그인 form 제공
|
||||
누락 또는 빈 값: 일반 설정 오류와 Dashboard 차단
|
||||
```
|
||||
|
||||
### login-failure-example — 잘못된 credential
|
||||
|
||||
- 대상 인터페이스: `dashboard-login`
|
||||
|
||||
#### 호출
|
||||
|
||||
```text
|
||||
관리자 ID와 password 제출
|
||||
```
|
||||
|
||||
#### 결과
|
||||
|
||||
```text
|
||||
최소 1초 후 "관리자 ID 또는 비밀번호가 올바르지 않습니다."
|
||||
POST /api/auth/login (same-origin HTTPS)
|
||||
admin_id=<submitted>&admin_password=<submitted>&remember_me=on
|
||||
→ 303 /
|
||||
→ Set-Cookie: __Host-jarca_dashboard_auth=<redacted>; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000
|
||||
```
|
||||
|
||||
## 검증
|
||||
|
||||
- 정상 계약: test-only credential과 synthetic DB를 사용한 AppTest 로그인 후 기존 페이지 렌더링
|
||||
- 실패 계약: 누락·빈 값·read error와 잘못된 ID·password의 일반 오류·무변경 검증
|
||||
- 경계 계약: query parameter 우회 금지, 새 session 재인증, 로그아웃 직후 차단, credential session 제거
|
||||
- 구현·스키마·예시 정합성: `dashboard/authentication.py`, `dashboard/app.py`, `.env.example`, `docker-compose.yml`과 인증 test를 함께 검토하며 DB migration이 없음을 확인
|
||||
- Token: `tests/test_dashboard_persistent_authentication.py`
|
||||
- HTTP endpoint/cookie: `tests/test_dashboard_auth_server.py`
|
||||
- Gate/DB ordering: `tests/test_dashboard_authentication_app.py`
|
||||
- HTTPS attributes/reconnect/logout: `scripts/persistent_auth_http_evidence.py`
|
||||
- 실제 secret/token 값은 test output과 evidence에 남기지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user