Gotanda.rb#46 "管理画面/権限" https://gotanda-rb.connpass.com/event/187557/ にて発表した内容です。
ニコニコ漫画と認可2020/9/29株式会社トリスタフサギコ(髙﨑 尚人)
View Slide
自己紹介• 名前:フサギコ• Twitter :fusagiko• GitHub :takayamaki• 本名 :髙﨑 尚人• 主な分野:Ruby(Rails), Terraform(AWS)• 他に :TypeScript, React, Ansible• Ruby:2.1.2~• 大学院生の時に修士論文用のシミュレータをPure Rubyフルスクラッチで書いたのが最初• 趣味:アイドルマスター• 全作品、それぞれ楽しんでるタイプ
自己紹介(OSS活動)• マストドン• 本家(tootsuite/mastodon)コントリビュータ• 2年弱PR送れてないけどまだcommit数37位らしい• imastodon.net構築、運用、管理人• アイドルマスターのファン向け非公式マストドン• 2017年4月開設• アカウント数4817、アクティブ数500/週程度• 本家に独自改造が入っている• 完全に袂を分かったわけではないのでVariety(変種)と自称• 追従は遅れ気味…
自己紹介(仕事)• ~ 2016/3 工学院大学大学院• 修士(工学) 修論:情報指向ネットワーク• 2016/4~2017/7 NHN テコラス株式会社• 2017/8~2019/6 株式会社ドワンゴ• friends.nico• ドワンゴ公式マストドン (サービス終了)• N Air• ニコニコ生放送向け 配信ソフト• 2019/7~現在 株式会社トリスタ• 読書メーター• 読書愛好家コミュニティサイト• ニコニコ漫画• UGC/PGC漫画連載プラットフォーム
ニコニコ漫画ニコニコ漫画
ニコニコ漫画UGC/PGC漫画連載プラットフォーム
ニコニコ漫画多くの編集部様からご連載頂いています
ニコニコ漫画はRailsで…ニコニコ漫画はRailsで作られている…
ニコニコ漫画はRailsではない現行はPHPかつモノリシック
ニコニコ漫画はPHP静画の派生として漫画が始まってから10年超え2010/8/27に特設ページ扱いで開始…らしいby Wikipedia
ニコニコ漫画はPHPコード構造が結構厳しい
ニコニコ漫画はPHPテストが十分でない
RailsでリプレイスしようビジネスロジックをRails APIサーバとして切り出し・リプレイスしよう
Railsでリプレイスしよう現行のPHPはBackends For Frontends的立ち位置に寄せていく
RailsでリプレイスしようそのRails APIサーバにおける認可機構の話をします
その前にと、その前に
認証と認可認証と認可それぞれの意味について念のため確認
認証と認可認証 authentication 認可 authorization“それ”が誰であるかを確かめることto prove that somethingis real“それ”に許可を与えることto give permission forsomething
このように認可と認証は異なる行為
認証と認可認可しない する認証しない何もわからないし何もさせない誰であるかは自己申告を信じ、操作を許可するするそれが誰かわかるが、何もさせないそれが誰かを識別し、誰であるかによって操作を許可する
認可 と 認証が間違われるのは全部TwitterのOAuthが悪いいわゆるログイン連携がしたいだけなら認証(”それ”の識別)さえできれば良かったはずなのに
全部TwitterのOAuthが悪い閑話休題
ニコニコ漫画における権限とはニコニコ漫画で権限管理が最も必要な場所
ニコニコ漫画で権限管理が必要な場所編集部様からの作品原稿の納入作品・各話のメタ情報編集公開期間の設定など
ニコニコ漫画で権限管理が必要な場所他編集部の公開前の作品が見えたり、編集できたりしてはならない
他編集部の作品が弄れてはならないこれを実現するために権限に基づく認可を行う
Rubyにおける権限管理のgemの選択肢Rubyにおける権限管理のgemの選択肢
Rubyにおける権限管理のgem•CanCanCan•Pundit•Banken
CanCanCan• ユーザを中心に、何へどのアクションを認可するかDSL的に記述• can 【アクション】, 【対象】, 【各種条件】• 対象にはActive Record継承したClassも指定できる• クエリインターフェイスに統合した形で使えて便利• Active Recordに関係ない対象はシンボルで表現• can? 【アクション】, 【対象】で判定• => Boolean• 対象がActive Recordモデルの場合• Article.accessible_by(current_ability) とかできる
Pundit• 認可系gemとしてはおそらくもっとも有名か• モデルを中心に、誰にどのアクションを認可するか記述• ActiveRecordモデルに対する操作タイミングで認可を行う• 複雑になってきたRailsアプリケーションではモデルとコントローラが一対一ではなくなりがち• コントローラごとに認可条件を変えたい場合詰む
Banken• コントローラのアクションメソッドと認可条件を紐づける• アクションを中心に、誰に許可するか記述• コントローラのアクションメソッドと対応付けるので対象リソースは自ずと導き出せるはず
認可系gemの概観権限記述の中心 判定タイミングPubdit モデル モデルへの操作Banken アクション コントローラのアクションCanCanCan ユーザモデルへの操作OR任意タイミング
ニコニコ漫画は権限判定にどのgemを使ったか
ニコニコ漫画はどれも採用しなかった
どの認可系gemも使わなかった• 現行PHPとDBを共有しながらの順次リプレイス• 現行が不完全なレベルベースの認可• モデルと必要権限が一対一で対応しない• 特定カラムの更新だけ上位権限が要る場合がある• アクションメソッドと必要権限も対応しない• 「このカラムの更新には上位権限が要る」といった知識をBFFに流出させてはならない• バルク(一括)操作がある• 唯一可能性があったのはCanCanCanだが…• どう考えても大規模になるのでメンテしづらそう• ユーザの権限情報のDB上の表現からCanCanCanのDSLに落とし込める確信が持てなかった
gemを使わない…というか使えない前述の通り、新ニコニコ漫画は既にRails的でない部分がある
決めた方針更なるRails的でない構造や大きな変更が襲来しても耐えられるようにしたい
そのためにインターフェイスを決めてコードの独立性を保ち、変更の影響範囲を狭める後続処理と重複したSELECTが多くなるのは妥協、許容する
認可によって知りたいのは誰がどのような操作をどれに対して行えるか
認可の最小限のインターフェイスauthorize(主体, 操作, 対象)=> boolean
認可の最小限のインターフェイスauthorize(user_id, action, target_ids)=> boolean
具体的利用例エピソード無料公開期間の作成を例として処理を追いかけるエピソード:第一話、第二話前半、単行本告知などニコニコ漫画上での公開単位
具体例:無料公開期間を作成するときApi::V1::FreeViewingTermsController#create
具体例:無料公開期間を作成するときざっくりとした各処理の区分バリデーション認可DB書き込みビューモデル & ビュー
具体例:無料公開期間を作成するときここで認可部を呼んでいる
認可部のエントリポイントauthorize!がauthorizeを呼ぶ
認可部のエントリポイントtarget_idsが単体な場合もKernel.#Arrayでカバー
認可部のエントリポイントtargetによって各Authorizerに分岐
認可部のエントリポイントtargetの種類はactionから導ける
認可部のエントリポイント無料公開期間のAuthorizerへ
無料公開期間のAuthorizer各Authorizerの中でまたactionで分岐
無料公開期間のcreate権限判定
無料公開期間のcreate権限判定まずEpisodeを引く
無料公開期間のcreate権限判定Episodeを不足なく取得できたか
無料公開期間のcreate権限判定公式連載でない作品のエピソードが混ざっていないか
無料公開期間のcreate権限判定権限付与の単位は作品なのでエピソードが属するComicをComic::OfficialComicとして引きなおす(ここ微妙ポイント)
無料公開期間のcreate権限判定ユーザの権限情報をDBから引いてくる
無料公開期間のcreate権限判定全作品対象の権限があったら早期return
無料公開期間のcreate権限判定権限の付与範囲ごとに認可された作品をArrayから取り除いていく
無料公開期間のcreate権限判定最終的に空ならば全て認可されている
Authorizer.authorize!に戻ってきて認可されたならばraiseされないので後続処理へ
以上が最も基本的な認可の仕組み
ところでDRYにはしたい他のアクションと判定基準が同じ場合も当然ある
権限判定の移譲アクションだけ差し替えて移譲先Authorizerのauthorizeメソッドを呼ぶ
ここで疑問Actionごとに渡すべきIDの種類が違うのでは?
Actionごとに渡すべきIDの種類が違うActionごとに渡すべきIDの種類が違うのでは?
Actionごとに渡すべきIDの種類が違うActionごとに渡すべきIDの種類が違うのでは?無料公開期間はエピソードに対して作るのでepisode_id
Actionごとに渡すべきIDの種類が違うActionごとに渡すべきIDの種類が違うのでは?無料公開期間はエピソードに対して作るのでepisode_id無料公開期間を消すときはfree_viewing_term_id
Actionごとに渡すべきIDの種類が違うActionごとに渡すべきIDの種類が違うのでは?無料公開期間はエピソードに対して作るのでepisode_id無料公開期間を消すときはfree_viewing_term_idepisode_idfree_viewing_term_idfree_viewing_term_id
IDの種類が混ざらないのはなぜか様々な種類のIDをどうやって区別しているのか
IDの種類を判別するしくみ• 各アクションのinterface class的なものを作る• 引数になりうるIDをインスタンス変数として持つ• attr_readerで読めるようにする• ID種別が非互換ならNo such methodで死ぬ• つまりダックタイピング
引数になるID種別が移譲先と違う場合移譲先のID種別に詰め替えるeql?とhashを実装するとuniqできる
未実装な機能• ユーザがINDEXできるものだけINDEXする機能• 現状、絞り込みのクエリパラメータが必ず付いているのでそれを起点に許可、拒否が決定できる• 現状、権限を持っている対象だけを絞ってINDEXしたいという要求がまだないので困っていない• それが来たときにどう実現するかは要検討• モデルのpublicメソッドにする?• どうにかしてActiveRecordスコープを作る?• CanCanCanのaccessible_byが参考になりそうか?
まとめ• ニコニコ漫画の現行はモノリシックなPHP• ビジネスロジックのRails APIモードへのリプレイスが進行中• 現行PHPはBFF的立ち位置に寄せていく• 認証と認可の定義• ニコニコ漫画では公式連載の原稿納入、公開期間設定などが認可の対象• Rubyの権限管理系gem 3種概観• 検討した結果、いずれも使わなかった• ニコニコ漫画の認可機構はどのように実装をしているか• 無料公開期間の作成を例とした具体的流れ• 判定基準が同じアクションは判定を移譲している• 多数あるIDの種類をどうやって区別しているか• 今後の課題• ユーザがINDEXできるものだけINDEXする機能
付録 ユーザの権限情報のDB上の表現Watch: 作品・エピソードのINDEXとSHOW, 公開期間のINDEXPublish: 公開期間のCREATE, UPDATE, DESTROYEdit: 作品・エピソードのUPDATE, DESTROY