Skip to content

Phase 4 PR #15 — 컴포넌트 승인 워크플로우 (/approvals)#18

Merged
haksungjang merged 3 commits into
mainfrom
feature/phase4-pr15-component-approval-workflow
May 8, 2026
Merged

Phase 4 PR #15 — 컴포넌트 승인 워크플로우 (/approvals)#18
haksungjang merged 3 commits into
mainfrom
feature/phase4-pr15-component-approval-workflow

Conversation

@haksungjang

Copy link
Copy Markdown
Contributor

Summary

  • 신규 component_approvals 테이블 + Postgres ENUM (approval_status) + Alembic 0008
  • 백엔드: 5개 엔드포인트 (/v1/approvals), ETag/If-Match optimistic concurrency, 상태 전이 매트릭스, RBAC, audit auto-emit
  • 프론트엔드: /approvals 페이지 + 드로어 (조건부 액션 버튼), TanStack Query, EN/KO i18n
  • 권한: developer (자신 팀 보기/생성), team_admin (전이), super_admin (cross-team) — existence-hide (비-팀원 = 404)
  • Domain Problem extensions: approval_already_open (409), approval_invalid_transition (409), approval_etag_mismatch (412), approval_terminal_state (409)

State machine

pending → under_review / rejected
under_review → approved / rejected
approved / rejected = terminal

Test plan

  • backend ruff clean
  • frontend npx tsc --noEmit clean
  • frontend vitest 419 tests pass (54 files), 91% line coverage
  • 백엔드 unit tests (16 pure + 13 DB-backed)
  • CI green (push 후 확인)

🤖 Generated with Claude Code

haksungjang and others added 3 commits May 8, 2026 16:13
## Summary
- 신규 component_approvals 테이블 + ENUM (approval_status) + Alembic 0008
- ComponentApproval 모델 (ETag 패턴: version 컬럼)
- 5개 엔드포인트: GET /v1/approvals (list), GET /:id (detail+ETag), POST (create), PATCH /:id/transition (If-Match), DELETE
- 상태 전이 매트릭스: pending → under_review/rejected → approved/rejected (terminal)
- 권한 매트릭스: developer (자신 팀 보기/생성), team_admin (자신 팀 전이), super_admin (cross-team)
- Existence-hide 패턴 (비-팀원 = 404)
- Optimistic concurrency (SELECT FOR UPDATE + ETag echo)
- Domain Problem extensions: approval_already_open / invalid_transition / etag_mismatch / terminal_state
- Audit emit (생성/전이/삭제 모두)
- 프론트: ApprovalsPage (필터/테이블/페이지네이션) + ApprovalsDrawer (조건부 액션 버튼)
- EN/KO i18n approvals namespace 동시 추가

## Test plan
- [x] backend ruff clean
- [x] frontend npx tsc --noEmit clean
- [x] frontend vitest 419 tests pass (54 files), coverage 91%
- [x] 백엔드 16+13 unit tests
- [ ] CI 9/9 green (push 후 확인)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…engine

- CurrentUser annotation referenced before import (mypy name-defined error);
  hoisted import to module level.
- Component model has no `version` or `kind` columns; updated fixture to use
  the actual schema (purl, name, package_type).
- Module-scoped async db_session_factory caused asyncpg "another operation
  in progress" cross-loop errors under pytest-asyncio's function-scoped loop;
  changed to function-scope with a session-once alembic gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- test_alembic_current_reports_head_revision: 0007 → 0008 after the
  component_approvals migration landed.
- Frontend functions coverage dropped to 79.86% because Step 1 introduced
  Home.tsx (Navigate) and ScansPage.tsx (ComingSoon stub) without tests.
  Added minimal smoke tests for both → coverage back to 80.33%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@haksungjang haksungjang merged commit 7699d52 into main May 8, 2026
haksungjang added a commit that referenced this pull request May 8, 2026
## Summary
### Notifications
- 신규 notifications/ 모듈
  - email.py: aiosmtplib 기반 SMTP 발송, MIME multipart (text + HTML alt)
  - slack.py: Slack Webhook POST (httpx.AsyncClient)
  - teams.py: MS Teams MessageCard
  - dispatcher.py: 통합 디스패처 (NotificationKind enum + per-channel dispatch)
- tasks/notify.py: Celery task with autoretry_for + exponential backoff (max 5회)
- 채널별 partial-success 보고 (어느 채널이 성공/실패했는지)

### Password Reset (Phase 6 미구현 부분)
- POST /auth/forgot-password: uniform 204 (CWE-204) — 이메일 존재 여부 누설 X
- POST /auth/reset-password: 토큰 + 새 비밀번호 (min_length 12)
- services/password_reset_service.py: 1회용 토큰 (used_at), 1시간 만료
- 기존 unused 토큰 invalidate (새 요청 시)
- email channel을 통해 reset link 발송

### 환경변수 (core/config.py)
- SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_USE_TLS
- SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL
- 모두 os.getenv() 런타임 호출 (CLAUDE.md §11)

## 보안
- token, password, secret 로그 X (PII 마스킹)
- timing-attack 방지: forgot-password에서 dummy bcrypt
- 토큰 평문 절대 저장 X (DB는 hash만)
- 1회용 + 만료 검증

## Test plan
- [x] backend ruff clean
- [x] backend mypy clean (226 source files)
- [x] notifications unit tests (slack/teams/email/dispatcher/notify task)
- [x] password reset integration tests
- [ ] Frontend (다음 PR)
- [ ] CI green (push 후 확인)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 8, 2026
* Phase 6 PR #18 (backend) — 알림 시스템 + 비밀번호 찾기

## Summary
### Notifications
- 신규 notifications/ 모듈
  - email.py: aiosmtplib 기반 SMTP 발송, MIME multipart (text + HTML alt)
  - slack.py: Slack Webhook POST (httpx.AsyncClient)
  - teams.py: MS Teams MessageCard
  - dispatcher.py: 통합 디스패처 (NotificationKind enum + per-channel dispatch)
- tasks/notify.py: Celery task with autoretry_for + exponential backoff (max 5회)
- 채널별 partial-success 보고 (어느 채널이 성공/실패했는지)

### Password Reset (Phase 6 미구현 부분)
- POST /auth/forgot-password: uniform 204 (CWE-204) — 이메일 존재 여부 누설 X
- POST /auth/reset-password: 토큰 + 새 비밀번호 (min_length 12)
- services/password_reset_service.py: 1회용 토큰 (used_at), 1시간 만료
- 기존 unused 토큰 invalidate (새 요청 시)
- email channel을 통해 reset link 발송

### 환경변수 (core/config.py)
- SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_USE_TLS
- SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL
- 모두 os.getenv() 런타임 호출 (CLAUDE.md §11)

## 보안
- token, password, secret 로그 X (PII 마스킹)
- timing-attack 방지: forgot-password에서 dummy bcrypt
- 토큰 평문 절대 저장 X (DB는 hash만)
- 1회용 + 만료 검증

## Test plan
- [x] backend ruff clean
- [x] backend mypy clean (226 source files)
- [x] notifications unit tests (slack/teams/email/dispatcher/notify task)
- [x] password reset integration tests
- [ ] Frontend (다음 PR)
- [ ] CI green (push 후 확인)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): resolve PR #22 CI failures (notify task, slack log, dispatcher, password-reset)

- tests/unit/tasks/test_notify.py: Use task.__wrapped__ instead of task.run.
  bind=True auto-injects self via .run; passing _FakeTask() explicitly added a
  6th positional arg.
- tests/unit/notifications/test_slack.py: Silence httpx/httpcore loggers and
  filter caplog records to notifications.* only — httpx's INFO log echoes the
  full webhook URL, which is not our module's leak.
- tests/unit/notifications/test_dispatcher.py: Override _send_email_channel
  with a real "no recipients raises NotificationDisabled" stub instead of
  trying to "restore" the already-noop'd version (autouse fixture had won).
- tests/integration/test_auth_password_reset.py: Replace AsyncSession.expire_all()
  (which needs a greenlet-bound context) with await commit() to force fresh
  reads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): split _run_notification helper for direct unit testing

Celery's bind=True decorator overrides task.run with self-injection
machinery and does not preserve __wrapped__, so neither approach (run nor
__wrapped__) gives the test a way to call the body with an explicit fake
self.

Extract the body into a plain `_run_notification` function and have the
Celery task delegate to it. Tests call `_run_notification` directly with
the _FakeTask self.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@haksungjang haksungjang deleted the feature/phase4-pr15-component-approval-workflow branch May 10, 2026 13:04
haksungjang added a commit that referenced this pull request May 26, 2026
`/admin/*` previously mounted under its own `AdminLayout` which rendered a
full 224px sidebar + 48px header in place of the AppShell. Entering Admin
therefore unmounted Dashboard / Projects / Scans / Approvals / Policies /
Integrations from the sidebar — users hit a dead-end where they couldn't
return to the rest of the portal without typing a URL.

Restructure the route table so `/admin/*` nests inside the AppShell route:

- `router.tsx`: move the entire admin Route group under the same `/`
  AppShell parent. The AppShell sidebar already renders an "Administration"
  section for super-admins (lines 188-199), so admin nav remains reachable
  alongside the main nav at all times.
- `AdminLayout.tsx`: strip the now-redundant chrome (sidebar / header /
  logout button / signed-in-as line). Reduced to the existence-hide guard
  (returns `AdminNotFound` for non-super-admins) wrapping `<Outlet />`.
  The `data-testid="admin-layout"` marker is preserved for harness compat.
- `AdminLayout.test.tsx`: drop assertions on the removed chrome
  (`admin-sidebar`, `admin-nav-*`, `admin-logout`); retain the three guard
  scenarios (super-admin / developer / no-user).
- `en/admin.json`, `ko/admin.json`: remove the unused `admin.layout.{title,
  signed_in_as}` keys (no other call sites).
- `docs/plan-w4-ui-ia-overhaul.md`: new plan doc — single source of truth
  for W4-A through W4-D.
- `docs/post-ga-execution-tracker.md`: mark W4-A "PR 대기".

Gates: typecheck clean · lint 0 errors · vitest 930 pass · i18n:check OK.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 29, 2026
…lier compat) (#289)

* feat(sbom): export licenses + real top-level version (SK Telecom supplier compat)

SBOM export previously emitted NOASSERTION for every SPDX license and omitted
CycloneDX `licenses` entirely, despite scans persisting real LicenseFinding
rows — an oversight, not a deliberate choice. The top-level component version
also used the scan UUID instead of a real software version.

- Load licenses (`_load_scan_licenses`): join license_findings JOIN licenses,
  group by component_version + kind, deduped + sorted (byte-stable).
- CycloneDX: per-component `licenses` from concluded -> declared -> detected;
  SPDX id -> license.id, else license.name. JSON + XML.
- SPDX: licenseDeclared / licenseConcluded as SPDX expressions (concluded
  falls back to declared). SPDX-id-only; LicenseRef-* -> NOASSERTION (follow-up).
  copyrightText stays NOASSERTION (no source yet).
- Top-level version: scan_metadata['release'] (Feature #18) when present, else
  scan id fallback -- no new input field.
- Log generic_purls count to surface pkg:generic components (rejected by some
  supplier programs); root-cause fix out of scope (cdxgen/input limitation).
- Tests: +13 (license id/name, concluded>declared, multi AND sorted, no-id
  NOASSERTION, no-license, byte-stability with licenses, version from release).
- Docs (EN+KO) + ADR 0003.

No schema change (data already persisted). Assessed against SK Telecom supplier
SBOM requirements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(sbom): document license export, top-level version, supplier compat

Follow-up to the SBOM license/version export feature. Updates the user guide
(EN + KO):
- "What's included per component": licenses (concluded > declared > detected;
  SPDX expression vs NOASSERTION; copyrightText still NOASSERTION) and
  top-level version (scan_metadata.release with scan-id fallback).
- "Supplier submission compatibility": SK Telecom supplier requirements coverage
  + caveats (pkg:generic rejection, LicenseRef NOASSERTION).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant