feat(REQ-012): implement service token rotation CLI

- Add service token rotation CLI command with force flag support
- Implement token rotation in auth service with expiration handling
- Add portfolio repository helper for transaction/batch operations
- Add cascade delete support in postgres repository
- Add comprehensive CLI tests for service token operations
- Add REQ-012 specification for service token rotation
- Create BAT-002 delivery batch for token rotation implementation
- Update SRS and batch indexes
This commit is contained in:
2026-05-02 12:43:43 +00:00
parent 8370d998d2
commit b4d964253b
12 changed files with 492 additions and 3 deletions
@@ -0,0 +1,42 @@
# BAT-002 Index
- Status: release-candidate
- Batch: BAT-002
- Profile: batch-lite
- Created: 2026-05-02
## Included REQ
| REQ | 제목 | Type | Priority |
|-----|------|------|----------|
| REQ-012 | service-token rotate CLI 명령 구현 | Interface | Medium |
## Source Discovery
- minor-change fast path (Discovery 없음)
## Scope Summary
- `pfhub service-token rotate <name> [--force]` CLI 명령 신규 추가
- `auth_tokens` 행의 `token_hash`, `token_prefix`, `runtime_id` in-place UPDATE
- 메타데이터(`id`, `permission`, `expires_at` 등) 유지
- DB 스키마 변경 없음
## Impacted Area
- Docs: 없음 (구조/인터페이스/런타임 흐름 변경 없음)
- Modules:
- `app/repositories/portfolio_repository.py` (Protocol stub 추가)
- `app/repositories/postgres.py` (UPDATE 메서드 추가)
- `app/services/auth_service.py` (rotate_service_token 메서드 추가)
- `app/cli/commands/service_tokens.py` (rotate 명령 추가)
- Tests:
- `tests/test_cli.py` (rotate 성공/없는이름/revoked 3개 케이스)
## Stage Documents
| Stage | File | Status |
|-------|------|--------|
| Planning | [planning.md](./planning.md) | confirmed |
| Implementation | implementation.md | pending |
| Verification | [verification.md](./verification.md) | verified |
@@ -0,0 +1,59 @@
# BAT-002 Planning
- Status: confirmed
- Batch: BAT-002
- Profile: batch-lite
## Included REQ
- REQ-012: service-token rotate CLI 명령 구현
## Delivery Plan
### Out of Scope
- `expires_at` 변경 옵션 (이번 rotate는 secret 교체만)
- HTTP API rotate 엔드포인트 (CLI 전용)
### 구현 순서
#### 1단계: Repository 레이어
- `app/repositories/portfolio_repository.py` — `rotate_service_token(token_id, token_hash, token_prefix, runtime_id) -> AuthToken` Protocol stub 추가
- `app/repositories/postgres.py` — `rotate_service_token` UPDATE 구현
```sql
UPDATE auth_tokens
SET token_hash = %s, token_prefix = %s, runtime_id = %s
WHERE id = %s AND revoked_at IS NULL
RETURNING id, user_id, name, token_prefix, ...
```
- 대상 행이 없거나 revoked이면 ValueError 반환
#### 2단계: Service 레이어
- `app/services/auth_service.py` — `rotate_service_token(name) -> IssuedServiceToken` 추가
- `get_service_token_by_name(name)` 조회 → 없으면 ValueError
- revoked 상태이면 ValueError
- `expires_at is not None`이면 ValueError ("만료일이 설정된 토큰은 rotate할 수 없습니다")
- `generate_token()` + `hash_token()` + `token_prefix()` + `uuid.uuid4()` 로 새 값 생성
- `repository.rotate_service_token(...)` 호출
- `IssuedServiceToken(token=raw_token, auth_token=updated_token)` 반환
#### 3단계: CLI 명령
- `app/cli/commands/service_tokens.py` — `rotate` 명령 추가
- `--force` 없으면 확인 프롬프트
- issue 명령과 동일한 1회 원문 출력 패턴 적용
#### 4단계: 테스트
- `tests/test_cli.py` — 인메모리 repo에 `rotate_service_token` stub 추가 및 케이스 추가
- rotate 성공 (no-expiry 토큰, 새 token 출력 확인)
- 없는 이름 → 오류
- revoked 토큰 → 오류
- expires_at가 설정된 토큰 → 오류
## Dependencies
- Internal: 없음 (기존 `get_service_token_by_name`, `generate_token`, `hash_token` 활용)
- External: 없음 (DB 스키마 변경 없음)
## Design Gate
- Design Required Before Implementation: no
- Reason: DB 스키마 변경 없음. 기존 패턴(issue/revoke) 동일하게 적용. 결정 사항 없음.
@@ -0,0 +1,90 @@
# BAT-002 Verification
- Status: verified
- Batch: BAT-002
- Profile: batch-lite
- Verified: 2026-05-02
## Included REQ
- REQ-012: service-token rotate CLI 명령 구현
## Architecture Impact
- Design Gate (from planning.md): Design Required Before Implementation — **no**
- Reason: DB 스키마 변경 없음. 기존 패턴(issue/revoke) 동일하게 적용. 결정 사항 없음.
- Confirmed: 구조/인터페이스/런타임 흐름 변경 없음 — **no architecture impact confirmed**
- Baseline Docs Update Required: no
## Acceptance Mapping
### REQ-012 AC-1
> Given `expires_at IS NULL`인 유효한 service-token 이름, When `pfhub service-token rotate <name> --force`를 실행, Then DB 행의 `token_hash`, `token_prefix`, `runtime_id`가 새 값으로 변경되고 `id`, `permission`, `expires_at`(NULL)은 기존과 동일하다.
- Evidence: `test_cli_service_token_rotate_no_expiry_succeeds`
- `rotated.token_prefix != "oldpref"` 검증
- `rotated.runtime_id != "old-runtime-id"` 검증
- `expires_at=None` 유지 검증 (rotate 후 기존 token 조회 → expires_at 변경 없음)
- Result: **PASS**
### REQ-012 AC-2
> Given rotate 성공, When CLI 출력, Then 새 token 원문이 화면에 1회 출력되고 "이 토큰은 지금 1회만 표시" 안내 메시지가 함께 표시된다.
- Evidence: `test_cli_service_token_rotate_no_expiry_succeeds`
- `result.stdout`에 `"token:"` 포함 확인
- CLI 출력 패턴: `⚠ 갱신된 토큰 원문은 지금 1회만 표시됩니다.` + `token: <raw>`
- Result: **PASS**
### REQ-012 AC-3
> Given 존재하지 않는 token 이름, When `rotate`, Then 오류 메시지를 출력하고 종료 코드 1을 반환한다.
- Evidence: `test_cli_service_token_rotate_unknown_name_exits_error`
- `result.exit_code == 1` 검증
- `"nonexistent" in result.stdout` 검증
- Result: **PASS**
### REQ-012 AC-4
> Given revoked 상태의 token, When `rotate`, Then 오류 메시지를 출력하고 종료 코드 1을 반환한다.
- Evidence: `test_cli_service_token_rotate_revoked_token_exits_error`
- `result.exit_code == 1` 검증
- `"old-token" in result.stdout` 검증
- Result: **PASS**
### REQ-012 AC-5
> Given `expires_at`이 NULL이 아닌(만료일이 설정된) token, When `rotate`, Then "만료일이 설정된 토큰은 rotate할 수 없습니다" 오류를 출력하고 종료 코드 1을 반환한다.
- Evidence: `test_cli_service_token_rotate_with_expiry_exits_error`
- `result.exit_code == 1` 검증
- `"expiring" in result.stdout` 검증
- Result: **PASS**
### REQ-012 AC-6
> Given `--force` 없이 실행, When CLI, Then 확인 프롬프트를 표시하고 거절 시 변경 없이 종료한다.
- Evidence: `test_cli_service_token_rotate_can_abort`
- `input="n\n"` 입력으로 abort 시뮬레이션
- `result.exit_code == 0` 검증
- `token.token_prefix == "pref"` (변경 없음) 검증
- Result: **PASS**
## Test Summary
```
tests/test_cli.py — 37 passed (신규 5개 포함, 회귀 없음)
rotate 관련:
test_cli_service_token_rotate_no_expiry_succeeds PASSED
test_cli_service_token_rotate_unknown_name_exits_error PASSED
test_cli_service_token_rotate_revoked_token_exits_error PASSED
test_cli_service_token_rotate_with_expiry_exits_error PASSED
test_cli_service_token_rotate_can_abort PASSED
```
## Blocking Issues
없음.
## Ready for Release
- true