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:
@@ -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
|
||||
@@ -8,4 +8,5 @@
|
||||
|
||||
| BAT ID | Profile | Status | Included REQ | Discovery | Folder |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| BAT-001 | standard | release-candidate | REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010 | dcy-002 | [bat-001_20260502_trex-basic-v1-api-contract](./bat-001_20260502_trex-basic-v1-api-contract/) |
|
||||
| BAT-001 | standard | release-candidate | REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010 | dcy-002 | [bat-001_20260502_trex-basic-v1-api-contract](./bat-001_20260502_trex-basic-v1-api-contract/) |
|
||||
| BAT-002 | batch-lite | release-candidate | REQ-012 | minor-change | [bat-002_20260502_service-token-rotate-cli](./bat-002_20260502_service-token-rotate-cli/) |
|
||||
@@ -0,0 +1,89 @@
|
||||
# req-012: service-token rotate CLI 명령 구현
|
||||
|
||||
- Status: Approved
|
||||
- Type: Interface
|
||||
- Priority: Medium
|
||||
- Owner: repository-maintainers
|
||||
|
||||
## Intent
|
||||
|
||||
- 발급된 service-token의 원문이 노출되었거나 주기적 교체가 필요한 경우, token을 revoke 없이 in-place로 갱신할 수 있는 CLI 명령이 필요하다.
|
||||
- 기존 `revoke → re-issue` 방식은 `token_id`, `user_id`, `permission`, `expires_at`, `created_at` 등 메타데이터가 변경되므로, 운영 시스템에서 동일한 이름으로 참조하는 통합 구성을 다시 설정해야 하는 불편이 있다.
|
||||
- rotate는 `token_hash`, `token_prefix`, `runtime_id`만 교체하고 나머지 메타데이터를 유지하여, 통합 구성 변경 없이 새 token 원문만 배포할 수 있게 한다.
|
||||
|
||||
## Requirement
|
||||
|
||||
- `pfhub service-token rotate <name> [--force]` CLI 명령을 구현한다.
|
||||
- rotate는 기존 auth_token 행의 `token_hash`, `token_prefix`, `runtime_id`를 새 값으로 UPDATE한다.
|
||||
- `id`, `user_id`, `name`, `permission`, `token_type`, `expires_at`, `created_at`은 변경하지 않는다.
|
||||
- rotate 대상 token이 존재하지 않으면 오류를 반환한다.
|
||||
- rotate 대상 token이 이미 revoked 상태이면 오류를 반환한다.
|
||||
- rotate 대상 token의 `expires_at`이 NULL이 아닌 경우(만료일이 설정된 토큰) 오류를 반환한다. rotate는 `--no-expiry`로 발급된 영구 토큰에만 허용된다.
|
||||
- `--force` 없이 실행하면 확인 프롬프트를 표시한다.
|
||||
- rotate 성공 시 새 token 원문을 1회 출력하고, 이후 list/조회에서는 token_prefix만 표시된다.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
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)은 기존과 동일하다.
|
||||
2. Given rotate 성공, When CLI 출력, Then 새 token 원문이 화면에 1회 출력되고 "이 토큰은 지금 1회만 표시" 안내 메시지가 함께 표시된다.
|
||||
3. Given 존재하지 않는 token 이름, When `rotate`, Then 오류 메시지를 출력하고 종료 코드 1을 반환한다.
|
||||
4. Given revoked 상태의 token, When `rotate`, Then 오류 메시지를 출력하고 종료 코드 1을 반환한다.
|
||||
5. Given `expires_at`이 NULL이 아닌(만료일이 설정된) token, When `rotate`, Then "만료일이 설정된 토큰은 rotate할 수 없습니다" 오류를 출력하고 종료 코드 1을 반환한다.
|
||||
6. Given `--force` 없이 실행, When CLI, Then 확인 프롬프트를 표시하고 거절 시 변경 없이 종료한다.
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs: docs/project-structure.md (변경 없음), docs/runtime-flows.md (변경 없음)
|
||||
- Modules:
|
||||
- `app/repositories/portfolio_repository.py` — `rotate_service_token` Protocol stub 추가
|
||||
- `app/repositories/postgres.py` — `rotate_service_token` UPDATE 구현 추가
|
||||
- `app/services/auth_service.py` — `rotate_service_token(name)` 서비스 메서드 추가
|
||||
- `app/cli/commands/service_tokens.py` — `rotate` 명령 추가
|
||||
- `tests/test_cli.py` — stub repo에 `rotate_service_token` 추가 및 테스트 케이스 추가
|
||||
- Tests:
|
||||
- `tests/test_cli.py`: rotate 성공, 없는 이름, revoked 상태 3개 케이스
|
||||
|
||||
## Notes
|
||||
|
||||
- `expires_at`은 rotate 시 변경하지 않는다. 만료일 변경이 필요하면 revoke + re-issue를 사용한다.
|
||||
- rotate는 `--no-expiry`로 발급된 영구 토큰(`expires_at IS NULL`)에만 허용된다. 만료일이 설정된 토큰은 rotate 대상이 아니다. 해당 토큰은 만료 후 re-issue하는 것이 권장 패턴이다.
|
||||
- DB 스키마 변경(migration)은 없다. 기존 `auth_tokens` 테이블의 기존 컬럼만 UPDATE한다.
|
||||
- 구조/인터페이스/런타임 흐름 변경 없음 → minor-change fast path 적용.
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260502-01
|
||||
- Date: 2026-05-02
|
||||
- Author: repository-maintainers
|
||||
|
||||
#### Change Summary
|
||||
- REQ-012 신규 작성 (minor-change fast path, Approved 직행)
|
||||
- rotate 허용 대상을 `expires_at IS NULL`(no-expiry) 토큰으로 제한
|
||||
|
||||
#### Intent
|
||||
- service-token rotate CLI 명령 필요성 확인 후 즉시 구현 진행
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added:
|
||||
- 6개 기준 신규 정의 (AC-5: expires_at 설정 토큰 rotate 거부 추가)
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Modules:
|
||||
- postgres.py, auth_service.py, service_tokens.py, test_cli.py 추가
|
||||
- Tests:
|
||||
- test_cli.py 3개 케이스 신규
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches:
|
||||
- BAT-002 (batch-lite)
|
||||
- Releases:
|
||||
- none (미정)
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: BAT-002 planning → implementation → verification 진행
|
||||
|
||||
#### Validation Plan
|
||||
- test_cli.py 3개 케이스 통과 + 기존 service-token 테스트 회귀 없음으로 완료 판단
|
||||
+3
-1
@@ -74,7 +74,7 @@
|
||||
|
||||
## Next Requirement ID
|
||||
|
||||
- Current: `REQ-012`
|
||||
- Current: `REQ-013`
|
||||
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
|
||||
|
||||
## Requirement Register
|
||||
@@ -92,9 +92,11 @@
|
||||
| REQ-009 | v1 API 지원을 위한 DB 스키마 migration 추가 | Migration | Approved | High | repository-maintainers | [REQ-009](./Migration/req-009_v1-api-db-schema-migration.md) |
|
||||
| REQ-010 | credential_payload Fernet 암호화 및 startup validation | Non-Functional | Approved | High | repository-maintainers | [REQ-010](./Non-Functional/req-010_credential-payload-fernet-encryption.md) |
|
||||
| REQ-011 | trex-basic 계약 반영을 위한 baseline 문서 갱신 | Documentation | Approved | Medium | repository-maintainers | [REQ-011](./Documentation/req-011_baseline-docs-update-for-trex-basic-contract.md) |
|
||||
| REQ-012 | service-token rotate CLI 명령 구현 | Interface | Approved | Medium | repository-maintainers | [REQ-012](./Interface/req-012_service-token-rotate-cli.md) |
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
- 2026-05-02: REQ-012 Approved 직행 (minor-change) — service-token rotate CLI 명령 구현
|
||||
- 2026-05-02: REQ-011 Approved 전환 — trex-basic 계약 반영을 위한 baseline 문서 갱신 (source Discovery: dcy-002)
|
||||
- 2026-05-02: REQ-010 Approved 전환 — credential_payload Fernet 암호화 및 startup validation (source Discovery: dcy-002)
|
||||
- 2026-05-02: REQ-009 Approved 전환 — v1 API DB 스키마 migration 추가 (source Discovery: dcy-002)
|
||||
|
||||
Reference in New Issue
Block a user