/note/tech

「技術負債にならない・間違えない」 権限管理の設計と実装

■ 1. 権限管理の重要性とアンチパターン

  • 権限管理ミスの影響: 給与や取引先情報などの機密情報公開につながり、サービスへの信頼が低下し、事業への大きな損失を招くため、ミスは許されない。
  • よくあるアンチパターン:
    • user.admin?user.super_admin? のように役割に依存した判定を行うこと。
    • 役割は事業やサービスの変化(市場、企業規模、機能)に伴い変わりゆくため、役割に依存した実装は変化に弱く、権限が暗黙的になり、技術負債の原因となる。
  • 推奨されるアプローチ: 役割(admin?)に依存せず、権限に依存した判定(例: user.can_create_project?)にすることで、権限が明示的になり、コードリーディングや変更時の整合性確認が容易になる。

■ 2. 権限管理の要素整理とPunditの問題点

  • 権限管理の要素分解: 権限管理を整理する上で、以下の4つの要素に分割できる。
    • 対象: Model(例: Project
    • 操作: CRUDやその他のアクション(例: update
    • 役割(RBAC): Role-Based Access Control(例: 管理者アカウント外部アカウント
    • 条件(ABAC): Attribute-Based Access Control(例: 作成者担当者
  • Punditの問題: Punditを利用した場合、一つのポリシーメソッド内で役割と条件の判定が混在し、複雑な判定ロジックになると、「通常ユーザーは更新できるか?」といった問いに対し、すぐに回答できない(理解で間違えやすい)という問題が生じる。
    • 例: 管理者かマネージャーかつ担当者か作成者ならできる... のような論理演算の連なりは、一目で理解しにくい。

■ 3. 要素を適切に分割したModuleの実装と実現方法

  • Moduleの設計思想: 役割と条件を分離し、権限を対象・操作・役割・条件の4要素で明示的に定義する。
  • 実装の概要:
    • ディレクトリ/ファイル構造: 対象ごとにディレクトリを作成し、その中に役割ごとにファイル(権限の具体クラス)を作成する。
    • 権限の判定: 権限の具体クラス内で、対象の基底クラスで定義した条件(例: assignee?author?)を英語と論理演算(||&&)でシンプルに組み合わせて表現する。
    • メタプログラミングの活用: 対象と役割から権限のクラスを特定する際に使用し、権限の追加・変更を容易にする。
  • 利用時のモード:
    • recordモード: 特定のレコードに対して権限があるかを判定する(例: Context.new.can_update?(@project))。
    • scopeモード: 権限があるレコードのみに絞り込む(例: Context.new.scope_for_read(Project))。
    • listモード: クライアント(Next.jsなど)に渡すための権限の一覧を取得する。

■ 4. 良い設計がもたらす影響

  • 間違えない実装の実現: 役割と条件を分離し、権限を明示的に記述することで、実装・利用・理解のすべてのフェーズで間違いを減らす(不具合の発生件数がゼロ、社内からの問い合わせがゼロ)。
  • 開発生産性の向上: CSやPdMがGitHub上のコードを直接見て理解できるようになり、エンジニアへの問い合わせ対応時間が削減された。
  • クライアント(Next.js)への影響:
    • Railsから渡された権限の一覧(JSON)にNext.jsが依存することで、Next.js側でも役割に依存しない実装が自然に実現した。
    • 権限によるUIの制御を(例: 編集ボタンの表示/非表示)宣言的に記述できるようになり、フロントエンドの技術負債も防いでいる。