
Kaigi on Rails 2025で登壇してきました。弊社は今年が初参加にもかかわらず3名が登壇の機会をいただきました。
社内ではプロポーザルの添削会を行いました。その際に話したプロポーザルのコツをまとめます。
プロポーザルは企画書
プロポーザルは話したいことを書くのではありません。コンテキストを共有していない選考者に1枠を使うだけの意義(コスパ)を説明する企画を書きます。
- なぜこの内容なのか?
- なぜ今なのか?
- どのような成果を想定しているか?
自分が話す意味や自分へのリターンは重視してはいけません。ただ、内容と事実がカンファレンスやコミュニティに対してどのようなメリットがあるか、それだけを考えればよいです。
有料のカンファレンスに登壇する。この時点で我々はプロです。
主張は1つ、情報は削ぎ落とす
ソフトウェア開発と同じで、関心事に集中できるように主張は1つにした方が良いです。
伝えられることは限られます。また、聴講者は当日は多くの話をずっっっと聞かされます。脳みそは疲れている前提で考えましょう。
タイトルと最後の着地が同じであるように、思考と主張の視点は固定であるように、意識しましょう。
概要(Abstract)で勝て
選考者は多くのプロポーザルを見ます。似た内容も多いでしょう。脳みそは疲れている前提で考えましょう。
概要(Abstract)で興味を得て、詳細(Details)で選ばれようとしないでください。概要(Abstract)で選ばれないと駄目です。
概要(Abstract)で選ばれてスタートラインで、詳細(Details)をしっかり見てもらえます。
概要(Abstract)で選ばれて、詳細(Details)で確実にしてください。
大事な3要素
Kaigi on Railsのような特定の技術のコミュニティでは、次の3つが大事だと考えています。
- あなたが対象だ!
- これは我々の敵だ!
- プレゼントがあるぞ!
自分事になってもらうこと、課題が一般的であること、それの対応策などの利を提供すること。
だから、お金を払って聞く価値がある。インターネットに残す価値がある。
先人の成功を参考にする
多くの方が記事を書いてくれています。参考にしましょう。
自分と似た領域や内容の登壇が過去にあるか、動機づけや構成はどうか、調べてみましょう。
実際に提出したプロポーザル
### Title 「技術負債にならない・間違えない」権限管理の設計と実装 ### 概要 Webサービスの開発と運用に関わる方なら、権限管理の重要性については深く認識していることでしょう。運営アカウント、役割などの条件によって表示や操作を変えたい際に必要となります。権限は非常に繊細であり、1つの実装ミスがサービスや事業の大きな損失に繋がります。 しかし、条件をそのまま表現した`admin?`のような簡易的な分岐による実装が多く見られます。そして、成長と共に`admin?`や`manager?`など権限の種類が増え、条件が複雑になり「レビュワーも条件を把握しきれない。」「実装を触るのが怖い。」「不具合の温床だ。」と技術負債になっているケースを多く目にしてきました。 このセッションでは、まず「なぜ権限管理は複雑になり、技術負債を生むのか」という根本原因を紐解きます。そして、その失敗から学んだ「実装・利用・理解、その全てで**間違えない**」というたった一つの原則に光を当てます。 私が辿り着いたのは、Rails Wayに沿ってModelとPolicyを1対1で対応させ「**更に1つ軸を設ける**」アプローチです。この設計によって、コードは驚くほど見通しが良くなり、エンジニア以外も誰が何をできるのかを正確に理解できるようになりました。 `admin?`を超えた先にある、明日からあなたのチームで実践できる「技術負債にならない・間違えない」権限管理の設計と実装。その全てをお伝えします。 ### 詳細 この発表では、以下に該当する聞き手に対して、権限管理の設計と実装に関する具体的な解決策を提供します。さらに、具体的なアンチパターンとその理由、そして良い設計がもたらすサービスと事業における利点についても共有します。 #### 想定する聞き手 このセッションでは、初級から上級の開発者を対象としています。特に以下のような聞き手には有益な内容となるでしょう。 - Webサービスの開発や運用に関与している方 - 権限管理の設計と実装に関心がある方 - 技術負債を避ける設計と実装に関心がある方 #### 話そうと考えていること 1. 権限管理のユースケース / 繊細さ / 技術負債 の具体例の説明(5分) - 何を実現したい際に登場するか - どのようなミスが起きうるか、サービスと事業に損失があるか - 技術負債の具体例 - 実装の多くの場所で`admin?`があるが、`admin?`が何ができるかわかない。と思ったら`super_admin?`が出てきたが!? - PunditなどGemはあるが条件がメンバー or 作成者 or 管理者 and ...と複雑でパッと理解できない! - アンチパターンの紹介 - 役割(`admin?`)でなく権限(`can_create?`)に依存した実装が望ましい - adminが作成をできるという暗黙的な仕様の上で成り立つ実装になってしまっているため、adminが何をできるかが変わる際に全ての`admin?`の使用箇所を確認しないといけない - → 聞き手に問題提起に対してあるあるの共感や危機感を与えて、解決策への意欲を引き出す 2. なぜ権限管理が複雑になるか(3分) - 権限の種類が増える - 権限の定義が変わる - → サービスや事業の成長に伴い、想定ユーザーが変わることや機能が増えることで当時の設計(線引)が最適ではなくなるという前提を聞き手と共有する 3. 権限管理で大事なこと(2分) - 間違えない - 実装で間違えない(追加する時) - 利用で間違えない(処理中に判定する時) - 理解で間違えない(問い合わせの回答の時) 4. Rails Wayに沿ってModelとPolicyを1対1で対応させ、「**更に1つ軸を設ける**」アプローチのModuleの詳説(15分) - Rails Wayに沿って権限を再整理 - 例として「プロジェクトの更新は、管理者か担当者ならできる」を考える - 抽象化すると、「対象の、操作は、役割か、条件ならできる」となります - Rails は schema・model・controller と対象を意識させるフレームワークのため、権限も対象を軸として整理する - ユーザー -> 役割 -> 対象 -> 条件 -> 操作(CRUD) -> ※属性(white list / カラム) - 更に1つ軸を設けるアプローチのModule - Punditなどで実装が複雑になるのは対象と操作の2つしか軸がないため、役割という軸を設けることで実装を簡潔にする - ※独自Moduleの実装は1番最後に載せています - 結果、エンジニア以外も誰が何をできるのかを正確に理解できるようになった - 実際に実装を提示して聞き手に「技術負債にならない・間違えない」を実感してもらいます 5. 良い設計がもたらすサービスと事業における利点(5分) - 不具合が0になりサービスの信用が上がった - 社内からのお問い合せが0になり開発生産性が上がった - エンジニアは聞かれたらコードを確認して回答するので10分は使ってしまう - CSやPdMが実装をみて回答できるため、エンジニアまで質問が来なくなった - 権限の一覧をクライアントに返す設計によって、Railsだけでなくクライアントのコードの技術負債も防げている - クライアントのコードも役割でなく権限に依存する実装に自然となった #### 参加者が得られる成果 - 権限管理の実装に対する具体的な解決策の理解 - 課題の整理と解決策(リファクタリング)の模索の思考方法 - 既に権限管理があるサービスに関わっている参加者も同様の工夫で改善が可能 - 良い設計がサービスや事業にもたらす具体的なメリットの理解 #### 独自Moduleの実装 ##### ディレクトリ構成 ``` app └── policies ├── policy │ ├── project # Project が対象の権限について │ │ ├── base.rb # Project の共通基底クラス │ │ └── roles │ │ ├── admin.rb # Project が対象の 管理者 の権限 │ │ └── normal.rb # Project が対象の 通常 の権限 │ ├── event │ │ ├── base.rb │ │ └── roles │ │ ├── admin.rb │ │ └── normal.rb │ ├── base.rb │ └── context.rb └── policy.rb ``` 権限の対象ごとにディレクトリを分けています。 model と対応させることで権限の対象が何かを明確にします。 また、対象ごとに base.rb を用意することで、ドメイン特有の権限や処理の拡張を実現しています。 そして、役割ごとにファイルを分けることでどんな境界(役割)があるのかを明確にします。 基本は同じ数のファイルを持つため、役割の定義漏れに気づきやすくなります。 ファイルに権限をすべて記述することで対象にはどの権限があるかを明確にします。 ##### policies/policy.rb ``` module Policy class Error < StandardError; end class NotAuthorizedError < Error attr_reader :policy, :action def initialize(options = {}) @policy = options[:policy] @action = options[:action] super("not allowed to #{policy.class.name}##{action}") end end class NotDefinedError < Error attr_reader :record_class, :role def initialize(options = {}) @record_class = options[:record_class] @role = options[:role] super("unable to find #{record_class} policy for #{role}") end end def self.authorize(user, record, action) context = Context.new(user:) context.authorize(record, action) end def self.authorize_scope(user, scope, action) context = Context.new(user:) context.authorize_scope(scope, action) end def self.permissions(user) context = Context.new(user:) context.permissions end end ``` 利用方法は 3 つあります。 ###### 1. 権限の判定を行う ``` project = Project.find(1) Policy.authorize(user, project, :update) ``` ###### 2. 権限があるものだけに絞り込む ``` scope = Project.all Policy.authorize_scope(user, scope, :update) ``` ###### 3. 権限の一覧を取得する ``` Policy.permissions(user) ``` ##### policies/policy/context.rb ``` module Policy class Context attr_reader :user def initialize(user:) @user = user end def authorize(record, action) raise(ArgumentError, 'record cannot be nil') unless record policy = policy_class(user, record.class).new(user:, record:, mode: :record) raise(NotAuthorizedError, policy:, action:) unless policy.public_send(action.to_sym) policy.record end def authorize_scope(scope, action) raise(ArgumentError, 'scope cannot be nil') unless scope raise(ArgumentError, 'scope must be ActiveRecord::Relation') unless scope.is_a?(ActiveRecord::Relation) policy = policy_class(user, scope.klass).new(user:, scope:, mode: :scope) policy.public_send(action.to_sym) end def permissions list = {} policy_constants.each do |constant| klass = policy_class(user, constant) policy = klass.new(user:, mode: :list) constant_result = klass.public_instance_methods(false).each_with_object({}) do |method, result| result[method.to_sym] = policy.public_send(method) end list[constant.to_s.underscore.to_sym] = constant_result end list end private def policy_constants reject_constants = [:Base, :Context, :Error, :NotAuthorizedError, :NotDefinedError] Policy.constants.reject { |constant| reject_constants.include?(constant) } end def policy_class(user, record_class) role = user.role.camelize "Policy::#{record_class}::Roles::#{role}".safe_constantize || raise(NotDefinedError, record_class:, role:) end end end ``` `user` の `role` と対象の `record` から、権限のクラスを取得しています。 メタプログラミングを利用することで、クラスを追加するだけで権限を追加できるようにしています。 `permissions` は、`user` の `role` の権限を一覧で取得しています。 この一覧をクライアントに提供することで、役割ではなく権限に依存した細かい制御を実現しています。 ```json:jsonの例 { "permissions": { "project": { "read": true, "create": true, "update": ["assignee"], "delete": false, "invite": false } } } ``` ##### policies/policy/base.rb ``` module Policy class Base attr_reader :user, :record, :scope, :mode # NOTE: mode: :list, :record, :scope def initialize(user:, record: nil, scope: nil, mode: nil) @user = user @record = record @scope = scope @mode = mode end def read raise(NotImplementedError) end def create raise(NotImplementedError) end def update raise(NotImplementedError) end def delete raise(NotImplementedError) end end end ``` ##### policies/policy/project/base.rb ``` module Policy module Project class Base < Policy::Base def assignee? record.assignees.exists?(id: user) end def assignee_scope scope.left_joins(:assignees).where(assignees: { id: user }).distinct end def invite raise(NotImplementedError) end end end end ``` 各対象の `base.rb` には、権限の具体的な処理とドメイン特有の権限を定義します。 例として「プロジェクトに招待できるか」の権限を `invite` として定義しています。 `ApplicationRecord` に対しての処理と `ActiveRecord::Relation` に対しての処理を分けています。 ##### policies/policy/project/roles/admin.rb, normal.rb ``` module Policy module Project module Roles class Admin < Base def read case mode when :list true when :record true when :scope scope end end def create case mode when :list true when :record true when :scope scope end end def update case mode when :list true when :record true when :scope scope end end def delete case mode when :list true when :record true when :scope scope end end def invite case mode when :list true when :record true when :scope scope end end end end end end ``` ``` module Policy module Project module Roles class Normal < Base def read case mode when :list true when :record true when :scope scope end end def create case mode when :list true when :record true when :scope scope end end def update case mode when :list [:assignee] when :record assignee? when :scope assignee_scope end end def delete case mode when :list false when :record false when :scope scope.none end end def invite case mode when :list false when :record false when :scope scope.none end end end end end end ``` 役割ごとにファイルを分けることで、コードが複雑になるのを防いでいます。 対象の基底クラスの関数を利用して、権限を表現しています。 例えば、メンバー かつ 担当者 または 作成者 のような判定もシンプルに表現できます。 ``` when :record member? && (assignee? || author?) ``` また、一覧の場合は具体的なデータが必要な判定はできないため、key を返すようにしています。 ### ピッチ #### このプロポーザルを採択すべき理由 以下に、このプロポーザルを採択すべき4つの理由を述べます。 1. **幅広い層に対応**: 権限管理は個人開発から大規模サービスまで利用される機能です。Kaigi on Railsの参加者の大半が関わったことのある権限管理を題材として課題の整理と具体的な解決策を伝えることで、初級者から上級者までに強い関心と学びを与えられると期待しています。 2. **独自性**: Railsにおいて、権限管理を実装する際はPunditやCanCanCanなどのGemを利用することが多いです。これらの成熟した方法ではなく、権限というものを見直すことで辿り着いた思想と独自Moduleを提示することで、新たな視点と議論の機会を提供します。 3. **実践的な内容**: 本セッションでは、課題の整理と思想だけでなく、それを実現する独自Moduleの実装も含みます。実際にコードを見ることで「技術負債にならない・間違えない」を実感でき、参加者の今後の開発にとって有用なものになると考えています。 4. **権限管理の設計の定番になれる**: Railsにおいて、権限管理を実装する際はPunditやCanCanCanなどのGemを利用することが多いです。Webを検索してもGemの利用以上の解説をしているサイトは少ないです。権限管理が繊細な実装であるが故に発信が少ないと考えられます。権限管理の設計と実装の定番として本セッション及びKaigi on Railsが認知されることを期待しています。 #### テーマで講演する資格 私はRuby on Railsを用いた開発を行っており、特に権限管理が重要とされる業務支援SaaSで、本セッションの主題である「技術負債にならない・間違えない」権限管理の設計と実装と運用を経験してきました。そのため、このテーマについて深く語るための十分な知識と経験を有しています。 例えば「自分が担当している案件だけ見れる。」「管理者だけが会計情報を見れる。」「協力会社は、顧客情報が見れない。」のような個社ごとに異なる要望の権限管理を本セッションで紹介する独自Moduleで実現しています。また、独自Moduleの導入後1年が経過していますが権限に関するお問い合わせ・不具合は1件も発生しておりません。 ### Bio 株式会社プレックスで「サクミル」の開発をしています。新卒からRailsでご飯を食べています。
添削会のSlackとNotionのコメント
▼ 主張は1つ、情報は削ぎ落とすについて

▼ yuhiの階層構造の初期プロポーザルへの添削

▼ 概要(Abstract)で勝てについて

まとめ
プロポーザルの提出は初めてでしたが、社内で知見を共有しながら進めることができて楽しかったです。
結果として、初参加にもかかわらず3名が登壇の機会をいただくことができました。
おまけ:登壇で意識したこと
私の登壇は2日目の最後でした。
聴講者は本当に疲れていると思ったため、気軽に聞けるポップな構成にしました。その上で「技術負債にならない・間違えない」を実感してもらえることを重視しました。
極論「なんとなく分かる人🙋」で手が上がれば良い発表!の認識で挑みました。
おまけ:Kaigi on Rails 2025 感想
テックリードであり、プロダクトチームの責任者という立場のため、普段は孤独な闘いが多いです。
カンファレンスに参加して、同じ技術を使い、同じように現実と闘う方々と話すことで、元気をいただいて気が引き締まりました。
事業とサービスがある程度の規模になり、発表できる内容が蓄えられているため、今後もコミュニティに少しでも還元できたらと考えています。
コメント