Skip to content

✨ org-accounts-foundation: Account 種別 + 組織作成 + メンバーシップ 3段階ロール (#55) - #56

Merged
turtton merged 12 commits into
mainfrom
org-accounts-foundation
Sep 1, 2026
Merged

turtton merged 12 commits into
mainfrom
org-accounts-foundation

Conversation

@turtton

@turtton turtton commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

org-accounts-foundation: Account kind 判別 + 組織作成 + メンバーシップ 3段階ロール

Summary

組織 Account 基盤を実装する。Account に personal/organization の kind 判別を導入し、
organization_members テーブル + CRUD repository + 組織作成/招待/承諾/ロール変更/除名/脱退/
所属組織一覧の client REST API を新設した。

Changes

  • Migration: accounts.kind TEXT NOT NULL DEFAULT 'personal' (+ CHECK)、既存レコードは
    'personal' backfill / organization_members (org_account_id, member_account_id, role, status, invited_by, created_at, PK(org,member), FK→accounts ON DELETE CASCADE)
  • Kernel
    • AccountKind { Personal, Organization } (#[serde(default)] で旧 Created event は Personal
      に後方互換)、Account に kind フィールド、Account::create_organization 新設
      (personal→organization 変換経路は存在しない)
    • OrganizationMembership エンティティ (OrgRole: Owner/Admin/Member, status: pending/active)
    • CRUD repository port (kernel/src/repository/organization_membership.rs) + Query facade
      (kernel/src/read_model/organization_membership.rs)、impl_database_delegation! 追加
  • Driver: PostgresOrganizationMembershipRepository (CRUD + owner count)、
    account projection/read model が kind を読み書き、組織 Account では Keto owner relation を
    作成しない (組織ロール判定は organization_members 直接クエリ。Keto Organization namespace
    は新設しない)
  • Application: service/organization/
    • create (作成者の personal Account が Owner membership を即取得)、invite (Owner/Admin→
      pending)、accept (本人のみ pending→active)、change role (Owner のみ、最後の Owner の降格
      は Rejected)、remove (Owner/Admin、Owner は除名不可)、leave (本人、最後の Owner は
      Rejected)、list my organizations
  • Server: OrgAccountApi facade + OrgAccountRouter
    • POST /api/v1/organizations、GET /api/v1/me/organizations、
      GET /api/v1/organizations/{org}/members (pending 含む)、
      POST .../invites、POST .../invites/{account_id}/accept、
      PUT .../members/{account_id}/role、DELETE .../members/{account_id} (self = leave)
    • エラーマッピングは共有 ErrorStatus 規約: PermissionDenied→403, NotFound→404,
      Rejected→422, 不正入力→400
  • OpenAPI: utoipa 登録 + openapi.json 再生成

Acceptance criteria 対応

AC 対応
accounts.kind + backfill migration 20260901000001_add_account_kind.sql
Created event kind 後方互換 #[serde(default)] + 既存 legacy regression test 維持
組織作成は新規 factory のみ Account::create_organization のみ kind=Organization を生成
POST /organizations で作成者 Owner 化 membership を Active Owner で同時作成
招待→承諾フロー pending→active、招待は member 一覧 (pending 含む) で確認可
ロール変更 Owner のみ / 除名 Owner 不可 / 最後の Owner 脱退不可 use case 層でガード (Rejected)
GET /me/organizations org account_id・名前・自分の role を返す
membership CRUD + port + Postgres + read model 新設
組織ロールは DB 直接判定、Keto Organization 新設なし 実装ノート通り
403/404/422 マッピング 既存 ErrorStatus マッピングに整合

Verification

  • cargo fmt --all -- --check: pass
  • cargo clippy --workspace --all-features -- -D warnings: pass
  • cargo test --workspace --lib: 228 passed / 0 failed / 150 ignored
    (kernel 96, application 87, driver 45。ignored はすべて DATABASE_URL 不在による
    既存 self-skip)
  • ユースケース単体テスト (in-memory mock repo/query): 作成・招待→承諾・ロール変更・
    除名・脱退・権限拒否・last-owner guard を網羅
  • 既存テストに回帰なし (Account 作成フロー含め全緑)
  • driver の membership CRUD roundtrip 統合テストは DATABASE_URL gated で整備済み
    (本 CI 環境では skip)

Out of scope (packet 通り)

認証コンテキスト切替 (org-accounts-auth-context)、Profile 移管、AP 連合、課金、
Warning イベント、Keto Organization namespace、組織解散フロー (既存 Account 削除に準ずる)。

Closes #55


Review round 1 (request-update) 対応

Blocking 1 (AC11 success-path tests): organization/tests.rs に追加

  • create_organization_persists_organization_owner_and_auth_link — kind=Organization + 作成者 Active Owner membership + auth link 呼び出しを assert
  • owner_changes_active_member_role_to_admin — member→admin 変更の永続化
  • owner_removes_active_member — active member 除名後に membership 削除

Blocking 2 (GET /api/v1/accounts への組織混入): account/read.rs get_all_accounts に AccountKind::Personal filter を追加 (他の find_by_auth_id 呼び出しは不変更)。回帰テスト get_all_accounts_excludes_organization_accounts 追加。組織一覧は AC7 の /me/organizations のみが担う。

Advisory 対応

  • (a) 非 member self-DELETE の 404: 維持。所属していない組織に member 行は存在しないため AC10 第2項 (存在しないメンバー→404) 側の挙動とする。AC10 第1項の 403 は他人の membership 操作に適用済み (remove は 403)。route の self-remove フォールバックは PermissionDenied 時のみなので意図に整合。
  • (b) last-owner TOCTOU: 修正。port に lock_active_owner_rows (SELECT ... FOR UPDATE on active owner rows) を追加し、owner demotion / owner leave の両方で lock→count→mutation を単一 transaction 化。
  • (c) invited_by: 修正。member 一覧で nanoid を返す (schema description/example + openapi.json 再生成済み)。

@turtton turtton added the intent-target intent-cli workflow label label Sep 1, 2026
…on in roundtrip test (#55)

Postgres timestamptz stores microsecond precision; comparing the
nanosecond-precision local expectation made the entity equality flaky
(CI coverage run 33505931615 failed on a sub-microsecond mismatch).
@codecov

codecov Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.74298% with 518 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.68%. Comparing base (48422b0) to head (18a1bef).

Files with missing lines Patch % Lines
server/src/route/account/organization.rs 0.00% 109 Missing ⚠️
application/src/service/account/read.rs 31.03% 100 Missing ⚠️
application/src/service/organization/list.rs 0.00% 97 Missing ⚠️
server/src/api/organization.rs 0.00% 74 Missing ⚠️
...r/src/database/postgres/organization_membership.rs 85.71% 43 Missing ⚠️
server/src/schema/organization.rs 0.00% 42 Missing ⚠️
kernel/src/read_model/organization_membership.rs 41.93% 18 Missing ⚠️
driver/src/database/postgres/account/mod.rs 79.16% 10 Missing ⚠️
application/src/service/organization/membership.rs 97.58% 8 Missing ⚠️
application/src/service/organization/create.rs 91.42% 6 Missing ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #56      +/-   ##
==========================================
- Coverage   76.28%   75.68%   -0.61%     
==========================================
  Files         200      209       +9     
  Lines       23200    24496    +1296     
==========================================
+ Hits        17698    18539     +841     
- Misses       5502     5957     +455     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@turtton turtton added the intent-pr-request-update intent-cli workflow label label Sep 1, 2026
@turtton

turtton commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Review: request-update (org-accounts-foundation review)

Semantic review 結果、request-update です。AC1-10 の実装本体は packet 設計に整合していますが、AC11 と既存 surface への副作用で 2 件の修正が必要です。worktree read-only 参照 + diff 精査で確認済み (head 8ad26ed)。

Blocking

1. AC11 単体テスト不足 (作成・ロール変更成功・除名成功が未検証)

application/src/service/organization/tests.rs には招待フロー・権限拒否・ガード系のテストがありますが、AC11 が列挙する 6 項目のうち 3 項目の成功パスがありません。create.rs に inline test もありません:

  • 作成 (create): org Account が kind=Organization で作成されること、作成者に Active Owner membership が付くこと、auth_emumet link されることを assert する単体テストがない
  • ロール変更 (成功): Owner が member→admin 等を変更し永続化されることを検証するテストがない (現状は last-owner 降格の拒否のみ)
  • 除名 (成功): Owner/Admin が active member を除名し membership が削除されるテストがない (現状は Owner 除名の拒否のみ)

"作成・招待フロー・ロール変更・除名・脱退・権限拒否が検証される" のうち 作成/ロール変更/除名 の成功系を tests.rs の mock fixture (MockAccounts / MockMemberships) で追加してください。

2. 既存 GET /api/v1/accounts に organization が混入する (packet 未要求の既存 surface 変更)

  • application/src/service/organization/create.rs:79-81 で org account を creator の auth account に link (この経路自体は packet baseline で容認)
  • しかし application/src/service/account/read.rs:28-31 の find_by_auth_id は kind filter なし、AccountDto には kind フィールドがないため、組織作成後に既存の個人 Account 一覧 API に organization が区別不能な形で混入する

packet はこの既存エンドポイントの挙動変更を要求していません。read.rs の一覧ユースケースで AccountKind::Personal に絞る filter を追加し、混入しないことを保証する回帰テストを追加してください (組織一覧は AC7 の /me/organizations が担う)。別解を取る場合は PR 本文で正当化してください。

Advisory (同 PR での対応推奨、blocking ではない)

  • AC10 edge: 非メンバーが自分自身の account_id を指定して DELETE した場合、membership.rs:428-433 の membership-not-found が 404 になります (route/account/organization.rs:130 は PermissionDenied のみ remove にフォールバック)。AC10 第1項 "所属しない組織のメンバー管理 API → 403" と第2項 "存在しないメンバー → 404" の交点で、現挙動は第2項寄りで防御可能ですが、意図確認または挙動の統一をお願いします
  • TOCTOU (last-owner guard): count_active_owners が mutation transaction の前・別 connection で実行されています (membership.rs:434-454)。並行する降格/脱退で active Owner が 0 になり得ます。count+mutation を同一 transaction 内で FOR UPDATE または条件付き DML にする対応を推奨します (後続 packet への分離を選ぶ場合は PR 本文に既知事項として明記)
  • invited_by の識別子不整合: member 一覧レスポンスで account_id は nanoid、invited_by は内部 BIGINT id を露出しています (application/src/service/organization/list.rs:117-123)。nanoid への統一を推奨します

修正後の手続

修正 push 後、CI green を確認して intent-pr-rereview-ready へラベルを付け替えて lead へ [herdr-relay] してください。writeback (ADR 0007 / packets.md) は host 側の design 管轄で closeout 後に実施するため、本 PR への同梱は不要です。

Lock active owner rows, count owners, and mutate membership in one transaction for owner demotion and leave paths.

Non-member self-leave remains NotFound: the requested membership row does not exist. PermissionDenied remains the contract for managing another account's membership without organization membership.
@turtton turtton added intent-pr-update-in-progress intent-cli workflow label intent-pr-rereview-ready intent-cli workflow label intent-pr-approved intent-cli workflow label and removed intent-pr-request-update intent-cli workflow label intent-pr-update-in-progress intent-cli workflow label intent-pr-rereview-ready intent-cli workflow label labels Sep 1, 2026
@turtton
turtton merged commit d01d424 into main Sep 1, 2026
12 checks passed
@turtton
turtton deleted the org-accounts-foundation branch September 1, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

intent-pr-approved intent-cli workflow label intent-target intent-cli workflow label

Projects

None yet

1 participant