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:
@@ -145,6 +145,20 @@ class FakeRepo:
|
||||
raise ValueError("토큰을 찾을 수 없습니다")
|
||||
token.revoked_at = datetime.now(timezone.utc)
|
||||
|
||||
def rotate_service_token(
|
||||
self,
|
||||
token_id: str,
|
||||
token_hash: str,
|
||||
token_prefix: str,
|
||||
runtime_id: str,
|
||||
) -> AuthToken:
|
||||
token = self._auth_tokens.get(token_id)
|
||||
if token is None or token.revoked_at is not None:
|
||||
raise ValueError("토큰을 찾을 수 없습니다")
|
||||
token.token_prefix = token_prefix
|
||||
token.runtime_id = runtime_id
|
||||
return token
|
||||
|
||||
def list_service_tokens(
|
||||
self,
|
||||
user_id: str | None = None,
|
||||
@@ -1044,3 +1058,111 @@ def test_cli_service_token_list_empty_message() -> None:
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No service tokens found." in result.stdout
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# service-token rotate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_cli_service_token_rotate_no_expiry_succeeds() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
# no-expiry 토큰 발급 (expires_at=None)
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="permanent",
|
||||
token_hash="oldhash",
|
||||
token_prefix="oldpref",
|
||||
permission="read",
|
||||
token_type="service",
|
||||
expires_at=None,
|
||||
runtime_id="old-runtime-id",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["service-token", "rotate", "permanent", "--force"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "token:" in result.stdout
|
||||
# token_prefix가 새 값으로 교체되었는지 확인
|
||||
rotated = repo.get_service_token_by_name("permanent")
|
||||
assert rotated is not None
|
||||
assert rotated.token_prefix != "oldpref"
|
||||
assert rotated.runtime_id != "old-runtime-id"
|
||||
|
||||
|
||||
def test_cli_service_token_rotate_unknown_name_exits_error() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["service-token", "rotate", "nonexistent", "--force"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "nonexistent" in result.stdout
|
||||
|
||||
|
||||
def test_cli_service_token_rotate_revoked_token_exits_error() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
token = repo.create_auth_token(
|
||||
user.id,
|
||||
name="old-token",
|
||||
token_hash="hash",
|
||||
token_prefix="pref",
|
||||
permission="read",
|
||||
token_type="service",
|
||||
expires_at=None,
|
||||
runtime_id="rid",
|
||||
)
|
||||
repo.revoke_auth_token(user.id, token.id)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["service-token", "rotate", "old-token", "--force"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "old-token" in result.stdout
|
||||
|
||||
|
||||
def test_cli_service_token_rotate_with_expiry_exits_error() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="expiring",
|
||||
token_hash="hash",
|
||||
token_prefix="pref",
|
||||
permission="read",
|
||||
token_type="service",
|
||||
expires_at=datetime(2027, 1, 1, tzinfo=timezone.utc),
|
||||
runtime_id="rid",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["service-token", "rotate", "expiring", "--force"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "expiring" in result.stdout
|
||||
|
||||
|
||||
def test_cli_service_token_rotate_can_abort() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="permanent",
|
||||
token_hash="hash",
|
||||
token_prefix="pref",
|
||||
permission="read",
|
||||
token_type="service",
|
||||
expires_at=None,
|
||||
runtime_id="rid",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["service-token", "rotate", "permanent"], input="n\n")
|
||||
|
||||
assert result.exit_code == 0
|
||||
# token_prefix 변경 없음
|
||||
token = repo.get_service_token_by_name("permanent")
|
||||
assert token is not None
|
||||
assert token.token_prefix == "pref"
|
||||
|
||||
Reference in New Issue
Block a user