feat(auth): owner-bound bot tokens and self-service bot management
This commit is contained in:
@@ -15,8 +15,10 @@
|
||||
- 시스템은 `admin` 권한을 제외한 모든 사용자/토큰 요청에서 `owner_user_id`가 인증 주체와 일치하는 포트폴리오만 조회/변경/삭제 가능해야 한다.
|
||||
- 시스템은 `admin` 권한 요청에서 소유권 제약 없이 포트폴리오 전체를 조회/변경/삭제할 수 있어야 한다.
|
||||
- 시스템은 소유권 불일치로 거부된 요청에 대해 `403 Forbidden`을 반환해야 한다.
|
||||
- 시스템은 인증 컨텍스트에 사용자 식별자가 없는 토큰이 소유권이 필요한 포트폴리오 작업을 요청하면 `403 Forbidden`을 반환해야 한다.
|
||||
- 시스템은 봇 principal에 소유 사용자 식별자(`bot_principals.owner_user_id`)를 저장해야 하며, 봇 토큰 인증 시 해당 값을 실효 사용자 식별자로 해석해야 한다.
|
||||
- 시스템은 사용자 귀속 정보가 해석되지 않는 토큰이 소유권이 필요한 포트폴리오 작업을 요청하면 `403 Forbidden`을 반환해야 한다.
|
||||
- 시스템은 기존 포트폴리오 데이터에 대해 마이그레이션 시 `owner_user_id`를 null 허용으로 추가한 뒤, 백필 완료 후 not null 제약을 적용할 수 있어야 한다.
|
||||
- 시스템은 기존 봇 principal 데이터에 대해 마이그레이션 시 `bot_principals.owner_user_id`를 null 허용으로 추가한 뒤, 백필 완료 후 not null 및 FK(`users.id`) 제약을 적용할 수 있어야 한다.
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Given 인증된 일반 사용자(또는 해당 사용자에 귀속된 토큰)가 `POST /portfolios`를 호출할 때, When 포트폴리오가 생성되면, Then 저장된 `owner_user_id`는 요청자 사용자 식별자와 동일해야 하며 요청 본문으로 다른 소유주를 지정할 수 없어야 한다.
|
||||
@@ -24,12 +26,13 @@
|
||||
3. Given 일반 사용자 A가 목록 조회를 수행할 때, When `GET /portfolios`를 호출하면, Then 응답 목록에는 A 소유 포트폴리오만 포함되어야 한다.
|
||||
4. Given `admin` 권한 사용자가 존재할 때, When `GET /portfolios` 또는 특정 포트폴리오 변경 API를 호출하면, Then 시스템은 소유권 검사로 요청을 거부하지 않아야 한다.
|
||||
5. Given 마이그레이션 적용 전 생성된 레거시 포트폴리오가 존재할 때, When ownership 마이그레이션을 적용하면, Then 데이터 손실 없이 `owner_user_id`가 백필되고 이후 읽기/쓰기 API가 실패 없이 동작해야 한다.
|
||||
6. Given 인증 컨텍스트에 사용자 식별자가 없는 bot 토큰이 포트폴리오 생성/수정/삭제를 요청할 때, When 소유권이 필요한 API를 호출하면, Then 시스템은 `403`을 반환해야 한다.
|
||||
6. Given 사용자 A에 귀속된 bot 토큰이 존재할 때, When bot 토큰이 `GET /portfolios/{portfolio_id}` 또는 변경 API를 호출하면, Then A 소유 포트폴리오에는 접근 가능하고 A가 아닌 사용자 소유 포트폴리오에는 `403`을 반환해야 한다.
|
||||
7. Given 인증 컨텍스트에 사용자 식별자가 해석되지 않는 토큰(예: DB 비연결 정적 토큰)이 포트폴리오 생성/수정/삭제를 요청할 때, When 소유권이 필요한 API를 호출하면, Then 시스템은 `403`을 반환해야 한다.
|
||||
|
||||
## Impacted Area
|
||||
- Docs: `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md`, `docs/technical/auth-and-token-lifecycle.md`, `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/technical/portfolio-ownership-and-auth-design.md`
|
||||
- Modules: `app/core/auth.py`, `app/api/portfolios.py`, `app/services/portfolio_service.py`, `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`, `app/domain/models.py`, `app/schemas/portfolio.py`, `migrations/versions/*`
|
||||
- Tests: `tests/test_app.py`, `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_domain_services.py`
|
||||
- Tests: `tests/test_portfolio_ownership.py`, `tests/test_users.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`
|
||||
|
||||
## Notes
|
||||
- 본 requirement는 인증 서비스 분리 여부와 무관하게 portfolio-hub 내부에서 반드시 충족되어야 하는 리소스 소유권 계약이다.
|
||||
@@ -72,3 +75,39 @@
|
||||
|
||||
#### Validation Plan
|
||||
- 사용자별 소유권 격리 통합 테스트, admin 우회 테스트, 마이그레이션 백필 테스트를 통해 계약 충족 여부를 확인한다.
|
||||
|
||||
### CHG-20260414-01
|
||||
- Date: 2026-04-14
|
||||
- Author: GitHub Copilot
|
||||
|
||||
#### Change Summary
|
||||
- 봇 principal의 사용자 귀속(`bot_principals.owner_user_id`) 및 봇 토큰 실효 사용자 매핑 규칙을 requirement에 추가했다.
|
||||
|
||||
#### Intent
|
||||
- 사용자별 봇 bearer token이 해당 사용자 포트폴리오 경계 내에서만 동작하도록 계약을 명확히 하기 위함이다.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added:
|
||||
- 사용자 귀속 bot 토큰의 소유권 허용/거부 기준 (AC6)
|
||||
- 사용자 식별자 미해석 토큰 거부 기준 (AC7)
|
||||
- Updated:
|
||||
- Requirement 본문의 bot 토큰 처리 규칙
|
||||
- Removed:
|
||||
- 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs:
|
||||
- `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md`
|
||||
- `docs/SRS/index.md`
|
||||
- `docs/SRS/traceability-matrix.md`
|
||||
- Modules:
|
||||
- `app/repositories/postgres.py`
|
||||
- `app/services/auth_service.py`
|
||||
- `app/schemas/auth.py`
|
||||
- `migrations/versions/20260414_0011_bot_principal_owner_user_id.py`
|
||||
- Tests:
|
||||
- `tests/test_portfolio_ownership.py`
|
||||
- `tests/test_users.py`
|
||||
|
||||
#### Validation Plan
|
||||
- 사용자 귀속 bot 토큰의 owner 허용/타 사용자 거부 시나리오와 봇 owner 백필 마이그레이션 적용 검증을 수행한다.
|
||||
|
||||
+1
-1
@@ -27,11 +27,11 @@
|
||||
| REQ-004 | 포트폴리오 소유주 기반 접근 제어 | Interface | Proposed | High | Justin | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` |
|
||||
|
||||
## Recent Change Log Summary
|
||||
- 2026-04-14 — `REQ-004`에 사용자 귀속 봇 principal(`bot_principals.owner_user_id`) 및 봇 토큰 실효 사용자 매핑 규칙(AC6, AC7)을 추가했다.
|
||||
- 2026-04-13 — `REQ-004` 포트폴리오 소유주(`owner_user_id`) 및 소유권 기반 접근 제어 requirement를 추가했다.
|
||||
- 2026-04-13 — `REQ-003`에서 태그 필터 기반 조회의 결정성 유지 책임을 소비 서비스(stock-picker)로 이관했다.
|
||||
- 2026-04-13 — `REQ-003`에서 `sourceVersion`/`sourceTimestamp` 요구를 제거하고 태그 필터 기반 조회 결정성 계약으로 정리했다.
|
||||
- 2026-04-13 — `REQ-003`의 소비자 표현을 특정 서비스 전용에서 태그 기반 조회 소비 서비스 일반 계약으로 정리했다.
|
||||
- 2026-04-13 — `REQ-002`의 소비자 표현을 특정 서비스 전용에서 다운스트림 소비자 일반 계약으로 정리했다.
|
||||
|
||||
## Status Transition Evidence
|
||||
- `Proposed` → `Approved`: Owner가 지정되고 requirement 문서 리뷰가 완료되며 acceptance criteria가 누락 없이 검토되어야 한다.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| REQ-001 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` | #2 | TBD | TBD | Approved | In Progress | 2026-04-10 |
|
||||
| REQ-002 | AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9 | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 |
|
||||
| REQ-003 | AC1, AC2, AC3, AC4, AC5 | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 |
|
||||
| REQ-004 | AC1, AC2, AC3, AC4, AC5, AC6 | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | TBD | TBD | `tests/test_portfolio_ownership.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`, local `pytest` pass (2026-04-14) | Proposed | In Progress | 2026-04-14 |
|
||||
| REQ-004 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | TBD | TBD | `tests/test_portfolio_ownership.py`, `tests/test_users.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`, local `pytest` pass (2026-04-14) | Proposed | In Progress | 2026-04-14 |
|
||||
|
||||
## Update Rules
|
||||
- REQ ID must exactly match the SRS requirement file prefix.
|
||||
|
||||
@@ -32,10 +32,16 @@
|
||||
- `DELETE /auth/tokens/{token_id}`
|
||||
- `POST /auth/bots`
|
||||
- `GET /auth/bots`
|
||||
- `GET /auth/bots/{bot_id}`
|
||||
- `POST /auth/bots/{bot_id}/tokens`
|
||||
- `GET /auth/bots/{bot_id}/tokens`
|
||||
- `DELETE /auth/bots/{bot_id}/tokens/{token_id}`
|
||||
- `DELETE /auth/bots/{bot_id}`
|
||||
- `POST /auth/me/bots`
|
||||
- `GET /auth/me/bots`
|
||||
- `POST /auth/me/bots/{bot_id}/tokens`
|
||||
- `GET /auth/me/bots/{bot_id}/tokens`
|
||||
- `DELETE /auth/me/bots/{bot_id}/tokens/{token_id}`
|
||||
|
||||
### 포트폴리오
|
||||
- `GET /portfolios`
|
||||
@@ -427,18 +433,46 @@ curl -X POST "http://localhost:8000/auth/tokens" \
|
||||
- `GET /auth/bots/{bot_id}/tokens`: admin only, `AuthTokenResponse[]`
|
||||
- `DELETE /auth/bots/{bot_id}/tokens/{token_id}`: admin only, `204`
|
||||
|
||||
소유권/권한 제약:
|
||||
|
||||
- bot principal은 `owner_user_id`를 필수로 가진다.
|
||||
- bot permission은 `read` 또는 `trade-write`만 허용한다.
|
||||
- bot 토큰 인증 컨텍스트의 실효 `user_id`는 bot owner(`owner_user_id`)로 해석된다.
|
||||
|
||||
#### 사용자 전용(Self-service) 봇 관리
|
||||
- `POST /auth/me/bots`: DB-issued `read` 이상 사용자 토큰, `CreateSelfBotPrincipalRequest` -> `BotPrincipalResponse`
|
||||
- `GET /auth/me/bots`: DB-issued `read` 이상 사용자 토큰, 본인 소유 bot 목록
|
||||
- `POST /auth/me/bots/{bot_id}/tokens`: DB-issued `read` 이상 사용자 토큰, `CreateBotTokenRequest` -> `IssuedBotTokenResponse`
|
||||
- `GET /auth/me/bots/{bot_id}/tokens`: DB-issued `read` 이상 사용자 토큰, `AuthTokenResponse[]`
|
||||
- `DELETE /auth/me/bots/{bot_id}/tokens/{token_id}`: DB-issued `read` 이상 사용자 토큰, `204`
|
||||
|
||||
제약:
|
||||
|
||||
- `/auth/me/bots*` 경로는 요청자 본인 소유 bot(`owner_user_id == auth_context.user_id`)에만 허용된다.
|
||||
- 타 사용자 소유 bot 접근 시 `403`.
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/auth/bots" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "rebalance-bot", "permission": "trade-write", "description": "nightly jobs"}'
|
||||
-d '{"name": "rebalance-bot", "owner_user_id": "user-1", "permission": "trade-write", "description": "nightly jobs"}'
|
||||
|
||||
curl -X POST "http://localhost:8000/auth/bots/bot-1/tokens" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "nightly", "permission": "trade-write", "expires_in_days": 90}'
|
||||
|
||||
curl -X POST "http://localhost:8000/auth/me/bots" \
|
||||
-H "Authorization: Bearer <USER_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "my-bot", "permission": "read", "description": "personal automation"}'
|
||||
|
||||
curl -X POST "http://localhost:8000/auth/me/bots/bot-1/tokens" \
|
||||
-H "Authorization: Bearer <USER_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "personal-key", "permission": "read", "expires_in_days": 30}'
|
||||
```
|
||||
|
||||
### 사용자
|
||||
@@ -543,13 +577,20 @@ curl -X POST "http://localhost:8000/users" \
|
||||
`CreateBotPrincipalRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin"`
|
||||
- `owner_user_id: str`
|
||||
- `permission: "read" | "trade-write"`
|
||||
- `description: str | null`
|
||||
|
||||
`CreateSelfBotPrincipalRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write"`
|
||||
- `description: str | null`
|
||||
|
||||
`CreateBotTokenRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin" | null`
|
||||
- `permission: "read" | "trade-write" | null`
|
||||
- `expires_in_days: int | null`
|
||||
|
||||
`CreateUserRequest`
|
||||
@@ -628,7 +669,8 @@ curl -X POST "http://localhost:8000/users" \
|
||||
|
||||
- `id: str`
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin"`
|
||||
- `owner_user_id: str`
|
||||
- `permission: "read" | "trade-write"`
|
||||
- `description: str | null`
|
||||
- `is_active: bool`
|
||||
- `created_at: datetime | null`
|
||||
|
||||
@@ -33,6 +33,31 @@
|
||||
- 환경변수 기반 정적 토큰: `PORTFOLIOHUB_*_TOKENS`
|
||||
- DB 저장 토큰: 로그인 세션/클라이언트 토큰/봇 토큰
|
||||
|
||||
봇 토큰 사용자 컨텍스트 정책:
|
||||
|
||||
- 봇 principal은 `owner_user_id`를 가진다.
|
||||
- 봇 토큰 인증 시 실효 사용자 식별자는 `bot_principals.owner_user_id`로 해석한다.
|
||||
- 따라서 소유권이 필요한 포트폴리오 API에서 봇 토큰은 소유 사용자 경계 내에서만 동작한다.
|
||||
|
||||
## 봇 관리 API 정책
|
||||
|
||||
- Admin 경로:
|
||||
- `POST /auth/bots`
|
||||
- `GET /auth/bots`
|
||||
- `GET /auth/bots/{bot_id}`
|
||||
- `DELETE /auth/bots/{bot_id}`
|
||||
- `POST/GET/DELETE /auth/bots/{bot_id}/tokens*`
|
||||
- Self-service 경로(사용자 본인 전용):
|
||||
- `POST /auth/me/bots`
|
||||
- `GET /auth/me/bots`
|
||||
- `POST/GET/DELETE /auth/me/bots/{bot_id}/tokens*`
|
||||
|
||||
제약:
|
||||
|
||||
- self-service 경로는 DB 사용자 컨텍스트가 있는 사용자 토큰에서만 허용한다.
|
||||
- self-service 경로는 요청자의 소유 봇(`owner_user_id == auth_context.user_id`)에만 허용한다.
|
||||
- 봇 권한은 `read` 또는 `trade-write`만 허용한다.
|
||||
|
||||
## 토큰 라이프사이클
|
||||
1. 사용자 로그인(`POST /auth/login`) 또는 토큰 발급 API 호출
|
||||
2. 토큰 해시 저장 및 prefix 관리
|
||||
|
||||
@@ -75,6 +75,7 @@ FOREIGN KEY (owner_user_id) REFERENCES users(id);
|
||||
- 2단계: admin이 아니면 ownership 검사 수행
|
||||
- ownership 검사 기준: `portfolio.owner_user_id == auth_context.user_id`
|
||||
- `auth_context.user_id`가 없는 요청은 ownership 필요한 엔드포인트에서 `403`
|
||||
- 봇 토큰(`token_type=bot`)의 `auth_context.user_id`는 `bot_principals.owner_user_id`로 해석
|
||||
|
||||
## Endpoint Policy Matrix
|
||||
|
||||
@@ -92,6 +93,16 @@ FOREIGN KEY (owner_user_id) REFERENCES users(id);
|
||||
| `DELETE /portfolios/{portfolio_id}` | admin | admin only | 기존 정책 유지 |
|
||||
| `POST/GET/PATCH/DELETE /portfolios/{id}/comments*` | read or trade-write | 적용 | 상위 포트폴리오 ownership을 재사용 |
|
||||
|
||||
## Bot Principal Ownership 정책
|
||||
|
||||
- `bot_principals.owner_user_id`는 필수이며 `users.id` FK를 가진다.
|
||||
- 봇 principal 생성 정책:
|
||||
- Admin: 임의 사용자 owner로 생성 가능
|
||||
- Self-service 사용자: 본인 owner로만 생성 가능
|
||||
- 봇 토큰 관리 정책:
|
||||
- Admin 경로는 전체 봇 대상으로 관리 가능
|
||||
- Self-service 경로는 본인 소유 봇 대상으로만 관리 가능
|
||||
|
||||
## 서비스/리포지토리 반영 포인트
|
||||
- Domain model:
|
||||
- `Portfolio`에 `owner_user_id` 추가
|
||||
@@ -116,13 +127,16 @@ FOREIGN KEY (owner_user_id) REFERENCES users(id);
|
||||
1. 기존 portfolios row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증
|
||||
2. users가 없는 상태에서 migration 실행 시 실패 동작 검증
|
||||
3. downgrade 시 인덱스/제약 해제가 순서대로 수행되는지 검증
|
||||
4. 기존 bot_principals row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증
|
||||
5. bot_principals.owner_user_id FK(`users.id`) 제약 생성/해제 검증
|
||||
|
||||
## API Authorization
|
||||
1. 사용자 A 소유 portfolio를 사용자 B가 조회하면 403
|
||||
2. 사용자 A 소유 portfolio를 사용자 B가 수정/매매/리셋하면 403
|
||||
3. admin이 타 사용자 portfolio 조회/수정 가능
|
||||
4. 일반 사용자의 `GET /portfolios`는 본인 소유만 반환
|
||||
5. user_id 없는 bot principal 요청이 ownership 필요 API 호출 시 403
|
||||
5. 사용자 A에 귀속된 bot 토큰은 A 소유 portfolio만 접근 가능
|
||||
6. 사용자 B가 A 소유 bot의 self-service 토큰 발급/조회/폐기를 시도하면 403
|
||||
|
||||
## API Creation
|
||||
1. `POST /portfolios`에서 owner_user_id가 인증 사용자로 저장되는지 검증
|
||||
|
||||
Reference in New Issue
Block a user