Spring におけるエンドポイントの列挙と IntelliJ IDEA の呼び出し階層による影響範囲調査

こんにちは、 NewsPicks でソフトウェアエンジニアをしているガニエです。

2019年に NewsPicks に新卒入社してから現在に至るまで、基本的には toC サービスである NewsPicks の上に顧客組織内に閉じたコミュニティを作って NewsPicks をコミュニケーションに役立てることができる toB サービスの NewsPicks Enterprise を作るチームで開発をしています。

今回はそんな NewsPicks Enterprise の開発の過程で必要になった網羅的な影響範囲調査をできるかぎり系統的に行うためにしたことの話です。

背景

toC サービスの上に新たに toB サービスを作ろうとすると、今まで自由に取得できた DB 上のリソースに閲覧制限をかける必要が出てきたりします。

今回は対応漏れがないよう DB アクセスに近い層で制限をかけたいので、該当リソースは Repository レベルで取得を制限することにします。 つまり、コミュニティに属するリソースがリクエストされた場合、コミュニティメンバーからのリクエストでなければリソースを Repository から返さないようにするのです。

ところが、サーバーは元からこういった閲覧制限を想定して作られていなかったりします。 例えば、ユーザーからのリクエストだけを考えるなら Repository で制限をかけるだけで良いのですが、内部ツールからのアクセスなども考慮する必要があり、その場合は特別に取得を許可するなどといった作業が必要になります。

したがって、リソースに閲覧制限をかけることによって他のロジックに出る影響を予め調査しておく必要があります。 無論筆者は膨大なコードベースを完全に把握している訳ではないので網羅的に影響範囲を調査しなくてはいけません。

エンドポイントの網羅的調査

全エンドポイントを列挙し、制限をかける対象に少しでも関係のありそうなものについてはどのような経路で情報を取得しているかを調べます。 後に行う Repository 側からの網羅的調査の裏付けに使うことができ、想定外の経路で情報を取得していないかを確認する上でも有益です。

NewsPicks のサーバーは Spring を利用しているので、開発環境で以下のようなエンドポイントを埋め込むことで全エンドポイントを列挙した JSON が取得できました。

(なお、IntelliJ IDEA Ultimate を利用している場合は エンドポイント一覧が備わっているようです。)

@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;

@RequestMapping("endpoints")
public ResponseEntity<List<Map<String, String>>> getEndpoints() {
    Map<RequestMappingInfo, HandlerMethod> mappings = requestMappingHandlerMapping.getHandlerMethods();
    return ResponseEntity.ok(mappings.entrySet().stream().map(entry -> {
        return new HashMap<String, String>() {{
            put("method", entry.getKey().getMethodsCondition().toString());
            put("path", entry.getKey().getPatternsCondition().toString());
            put("handler", entry.getValue().getShortLogMessage());
        }};
    }).collect(Collectors.toList()));
}

出力は整形すると次のようになりました。

[
  {
    "path": "[/hogehoge]",
    "handler": "com.newspicks.foo.bar#hogehoge[1 args]",
    "method": "[POST]"
  },
  {
    "path": "[/fugafuga]",
    "handler": "com.newspicks.foo.bar#fugafuga[0 args]",
    "method": "[GET]"
  },
   ...

JSON 形式で取得したのですが、上から順に網羅的に調査するにあたり、チェックリストのような形式にしたくなりました。

このような目的には CSV が都合が良いでしょう。CSV にするには jq が役立ちました。

$ jq -r '.[] | [.method, .path, .handler] | @csv' < endpoints.json
"[POST]","[/hogehoge]","com.newspicks.foo.bar#hogehoge[1 args]"
"[GET]","[/fugafuga]","com.newspicks.foo.bar#fugafuga[0 args]"
...

あとは気合で一つずつ見ていきます。

Repository 側からの網羅的調査

今回の閲覧制限は Repository にある取得メソッドに修正を加えることで実施します。

修正を加えた取得メソッドの全ての呼び出し元を再帰的に辿って行けば、原理的には影響範囲を網羅することができるはずです。

これには IntelliJ IDEA の 呼び出し階層 (Call Hierarchy) が役立ちました。

f:id:takuyagagne_ub:20210218153820p:plain
呼び出し階層 (Call Hierarchy)

矢印をクリックすることで一階層ずつ Call Hierarchy が展開でき、Export to Text File ボタンをクリックすると展開した状態の木構造をテキストファイルに書き出すことができます。

今回は 2階層程度までの展開でかなり影響範囲が細分化できたので、途中までの展開でテキストファイルに書き出しました。

次に、テキストファイルに書き出した木構造をスプレッドシートに貼り付けて、木構造の全ての葉について

  • どのエンドポイントで使われているか
  • どのような対応が必要か

といったことを気合で調査・記載していきました。

最後に、抜け漏れがないかを先に作成しておいたエンドポイントのチェックリストとも照らし合わせて確認しました。

まとめ

既存のメソッドの挙動が変化するような修正を行う際は、何らかの手段により影響の有無と範囲を特定する必要があります。 閲覧制限のような権限に関わる修正はその典型的な例ではないかと思われます。

今回は Call Hierarchy だけで網羅できると考えられましたが、エンドポイント一覧によって裏付けし、想定外の取得経路がないかも確認しました。

最終的には気合でスキャンする泥臭い作業になることは変わりませんが、こういった泥臭くて細心の注意を要する作業ほど抜け漏れや手戻りがないように系統立てて行いたいものです。

© Uzabase, Inc. All rights reserved.