Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: GitLab 14.0のbreaking changes、Railsのセキュリティ脅威解説シリーズ記事ほか(20210628前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は、以下の公式更新情報から見繕いました。今回載せきれないほど更新がありましたので、来週も追ってみたいと思います。


つっつきボイス:「今回の公式更新情報、すごく多いですね」「Railsウォッチの先週の改修とのかぶりも少なかったので、もしかして追いつかれないように気合い入れてたりして」「ないない😆」

🔗 set_pool_configpool_configがnilの場合にエラーを出すようにした


つっつきボイス:「コネクションプールでマルチプルデータベースのロールやシャーディングなどを設定するときに、pool_configがnilならエラーをraiseするようにしたようですね」

# activerecord/lib/active_record/connection_adapters/pool_manager.rb#L38
      def set_pool_config(role, shard, pool_config)
-       @name_to_role_mapping[role][shard] = pool_config
+       if pool_config
+         @name_to_role_mapping[role][shard] = pool_config
+       else
+         raise ArgumentError, "The `pool_config` for the :#{role} role and :#{shard} shard was `nil`. Please check your configuration. If you want your writing role to be something other than `:writing` set `config.active_record.writing_role` in your application configuration. The same setting should be applied for the `reading_role` if applicable."
+       end
      end

参考: Shard (database architecture) - Wikipedia


#41549で、pool_confignilだったのでall_connection_poolsメソッドがエラーになったユーザーがいた。再現用アプリを入手すると、アプリケーションのコンフィグをミスったときにこれが発生することがわかった。たとえば、アプリケーションでwritingロールに:allを使ってもconfig.active_record.writing_role = :allが設定されず、setup_shared_connection_poolwriting_pool_configの値がnilになり、それがset_pool_configに設定されていた。
setup_shared_connection_poolを直接修正してエラーを出すようにすることも検討したが、外部gemやアプリケーションがprivate APIを使っているとこのエラーが発生する可能性がある。現実には、Railsであるかどうかに関わらず、どのコードもプールのproolコンフィグにnilを設定して欲しくない。

注: テストでは別途コネクションハンドラを作成して、テスト対象に別のプールを持たせるようにした。そうでないと既存のプールがテストされてしまうので、そちらに影響を与えたくない。
同PRより大意

🔗 forced_encoding_for_deterministic_encryptionオプションの追加など


つっつきボイス:「deterministic?」「"決定論的な"と訳されることが多いですね」「元が同じ文字列なら暗号化した結果も常に同じになる暗号化をdeterministic encryptionと呼びますね: この場合、暗号化済み文字列同士で同値かどうかを比較できます」「なるほど、そういう意味ですか」「deterministic encryptionでも元の文字列のエンコードが違えば同じにならなくなるので、同じになるはずのものがならないことがある問題を修正したようですね: テストでもエンコードをUS-ASCIIとUTF-8にして比較している↓」

# activerecord/test/cases/encryption/encryptable_record_api_test.rb#94
  test "encrypt will honor forced encoding for deterministic attributes" do
    ActiveRecord::Encryption.config.forced_encoding_for_deterministic_encryption = Encoding::UTF_8

    book = ActiveRecord::Encryption.without_encryption { EncryptedBook.create!(name: "Dune".encode("US-ASCII")) }
    book.encrypt
    assert Encoding::UTF_8, book.reload.name.encoding
  end

「いつも暗号化済みで同値比較できる方がよさそうですけど?」「カーディナリティの低いデータ(年齢や性別、誕生日など)を想定したときに、Aさんの暗号化前データを知っていればAさんと同じ情報を持つ人を特定できることになります」「あ、それはマズそう」「いえ、それだけでマズいというものではなく、そういう方式の暗号化もあるということです: 暗号化方式を選ぶときにはそうした使い勝手や運用も含めて検討する必要があります」「なるほど」

参考: Deterministic encryption - Wikipedia

ActiveRecord::Encryptionで"決定論的"暗号化を使う場合には値をUTF-8でエンコードするようになった。このエンコードは暗号化済みペイロードの一部なので、値によってエンコード方式が変わると暗号文も異なってしまう。これによってunique制約やクエリが壊れる可能性がある。
新しい振舞いはactive_record.encryption.forced_encoding_for_deterministic_encryptionでコンフィグ可能。デフォルトはEncoding::UTF_8で、nilを設定すると無効にできる。
Jorge Manrubia
Changelogより大意

他にも以下が追加されています。

  • 暗号化済み属性でexists?をサポート。
EncryptedBook.exists?(name: "Dune")
  • ignore_case: trueオプションを指定しても再暗号化で大文字小文字を区別するようになった。

🔗 strict_loadingがthrough関連付けの中間レコードへカスケードするようになった


つっつきボイス:「ここで言うカスケードって何だろう?」「このテスト↓を見るとDeveloper.strict_loading.includes(:firms)strict_loadingしたものがdev.firms.first.contracts.firstなどのメソッドチェーンでも効くようにしたということみたい」「なるほど」「明示的にstrict_loadingするならこういうふうに動いて欲しいでしょうね👍」

# activerecord/test/cases/strict_loading_test.rb#256
  def test_strict_loading_with_has_many_through_cascade_down_to_middle_records
    dev = Developer.first
    firm = Firm.create!(name: "NASA")
    contract = Contract.create!(developer: dev, firm: firm)
    dev.contracts << contract
    dev = Developer.strict_loading.includes(:firms).first

    assert_predicate dev, :strict_loading?

    [
      proc { dev.firms.first.contracts.first },
      proc { dev.contracts.first },
      proc { dev.ship }
    ].each do |block|
      assert_raises ActiveRecord::StrictLoadingViolationError do
        block.call
      end
    end
  end

🔗 package.jsonでカレントのRails->npm_versionを使うようになった


つっつきボイス:「ちょうどさっきセマンティックバージョニングの話をしましたけど(後述)、まさにそれに通じる改修かも」「5.0.0.rc15.0.0.beta1.1というバージョン表記だとnpmのバージョニングシステムに合致しないのか」「Rubygemは5.0.1.1のような4桁表示を認識できますけど、npmだと認識できないから5.0.1-1のような表記に置き換えるようですね」

参考: Semantic Versioningの闇 - knqyf263's blog

# railties/lib/rails/generators/app_base.rb#L301
+     # This "npm-ifies" the current version number
+     # With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its
+     # versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively.
+     #
+     # "5.0.1"     --> "5.0.1"
+     # "5.0.1.1"   --> "5.0.1-1" *
+     # "5.0.0.rc1" --> "5.0.0-rc1"
+     #
+     # * This makes it a prerelease. That's bad, but we haven't come up with
+     # a better solution at the moment.
+     def npm_version
+       # TODO: support `options.dev?`
+
+       if options.edge? || options.main?
+         # TODO: ideally this would read from Github
+         # https://github.com/rails/rails/blob/main/actioncable/app/assets/javascripts/action_cable.js
+         # https://github.com/rails/rails/blob/main/activestorage/app/assets/javascripts/activestorage.js
+         # https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts -> not clear where the output file is
+         "latest"
+       else
+         Rails.version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s }
+       end
+     end
+
+     def turbolinks_npm_version
+       # since Turbolinks is deprecated, let's just always point to main.
+       # expect this to be replaced with Hotwire at some point soon.
+       if options.main? || options.edge?
+         "git://github.com/turbolinks/turbolinks.git#main"
+       else
+         "^5.2.0"
+       end
+     end

「ついでにドキュメントも更新されてる↓」「Rails 7という文字を見てちょっとゾクゾクしました」

# guides/source/upgrading_ruby_on_rails.md#19
### Ruby Versions

Rails generally stays close to the latest released Ruby version when it's released:

* Rails 7 requires Ruby 2.7.0 or newer.
* Rails 6 requires Ruby 2.5.0 or newer.
* Rails 5 requires Ruby 2.2.2 or newer.

It's a good idea to upgrade Ruby and Rails separately. Upgrade to the latest Ruby you can first, and then upgrade Rails.

🔗 セマンティック バージョニングよもやま

「半年前の記事ですが、RubyやNode.jsを例に出していました」「記事冒頭の要約に大事なことは書かれていますね: バージョンの比較とバージョン制約は別の話」

  • Semantic Versioning 2.0.0にはバージョン"比較"の定義はあるが、バージョン"制約"(>= 2.1.3みたいなやつ)の定義がない
  • その結果、同じsemver準拠ライブラリでも制約の解釈が異なり結果が真逆になる
  • というかそもそもsemver使ってるエコシステムが少なすぎる
    Semantic Versioningの闇 - knqyf263's blogより

「Semantic Versioningはひと頃かなりメジャーになりましたね↓」「お〜、こんなガイドラインもあるんですか」「こうしたルールを何らかの形で決めておかないとRubyのbundlerのようなものが作れません」「それもそうか」

参考: セマンティック バージョニング 2.0.0 | Semantic Versioning

v1.2.3みたいにvを付けるのはセマンティックバージョンではないそうです↓」「え、vダメなのか」「この2.0ドキュメントではBNFまで使ってバージョンの書き方決めてる」「そうしないと壊れるからでしょうね」

『v1.2.3』はセマンティック バージョンでしょうか?
 いいえ、『v1.2.3』はセマンティック バージョンではありません。しかしながら、セマンティック バージョンに接頭辞の『v』を付けるのは英語ではバージョン番号であることを示す一般的な方法です。バージョン管理では、『バージョン』を『v』と略すことがよくあります。たとえば git tag v1.2.3 -m" Release version 1.2.3 " では『v1.2.3』はタグ名であり、セマンティック バージョンは『1.2.3』です。
semver.orgより

参考: バッカス・ナウア記法 - Wikipedia

「Semantic Versioningに沿っていると謳っているソフトウェアでも実際に厳密に沿っているとは限らないことが割とありますよ」「へ〜」「X.Y.Z(Xがメジャー、Yがマイナー、Zがパッチ)の3桁形式は取り入れていても、細かい点が違っていたりするのも見かけます: Railsもここで言うSemantic Versioning (SemVer)2.0.0には従っていないと言えますが『意味付けをしたバージョニング』という意味ではバージョンの付け方はちゃんと管理されているので、広義ではセマンティクスのあるバージョニング、という言い方もできると思います」「なるほど」

参考: Maintenance Policy for Ruby on Rails — Ruby on Rails Guides

Rails follows a shifted version of semver:
edgeguides.rubyonrails.orgより

「Railsのバージョンアップのインパクトとしては、Semantic Versioningで言うYのバージョンアップが事実上メジャーバージョンアップに近いものを感じますね」「まあたしかに😁」「Railsではセキュリティパッチのリリースに4桁目も付けますけど、こちらの方がパッチバージョンに近い気がしています」

「記事にもRubygemsのバージョニングは独特とありますね」「Rubygemのバージョニングとnpmパッケージのバージョニングなどもそうですけど、単に表記が違うだけでなく意味づけも違ったりすることがあるんですよ」「バージョニングって大変...」「固有名詞としての『Semantic Versioning(SemVer)』は厳密に定義を決めたものであるのに対し、世の中ではSemVerを参考にした『セマンティクスのあるバージョニングポリシー』の方言が色々あって、それらが混在してしまっているのが現状ですね」

参考: Representational State Transfer - Wikipedia


後で仕様のリポジトリを見つけました。

semver/semver - GitHub

オンラインのsemverバリデータも見つけました(公式ではないようです)。

🔗 Active StorageでGCSのcache_control:にデフォルト値の設定がサポートされた


つっつきボイス:「これはわかりやすいですね: Google Cloud Storage(GCS)のcache_controlにデフォルト値を書けるようになった」

gcs:
  service: GCS
  ...
  cache_control: "public, max-age=3600"

参考: Cloud Storage  |  Google Cloud

🔗Rails

🔗 GitLab 14.0のbreaking changes


つっつきボイス:「お、GitLabのメジャーバージョンアップきた: 近々にアップグレードしようかな」「GitLabのバージョンアップはmorimorihogeさんがやってるんですか?」「1〜2か月に1回ぐらいのペースで気が向いたときにやってます: GitLabのOmnibusパッケージで上げるだけなので随分楽になりましたよ」「へ〜、どんなふうにやってます?」「そんなに大変ではないですね、Ubuntuのパッケージがあるので基本的にはapt-get upgradeしますが、公式のアップグレードガイドにも推奨手順が書いてある↓のでそれに沿って進めます: 注意すべきはアップグレードパスで、基本的にマイナーバージョンを1つずつアップグレードします」

参考: Upgrading GitLab | GitLab

「GitLab 14.0にはbreaking changesがあるようなのでチェックするか: GraphQLフィールドの一部がdeprecatedになるのね」

「初期ブランチがmasterからmainに変わるんですね」「ついにGitLabもか」「最初のうちmainって打つときに違和感ありましたけど最近慣れてきました」

「"WIP merge request"の呼び方が"draft merge request"に変わるのは、GitHubの命名に寄せた感じかな」「タイトルがWIP[WIP]で始まるとマージボタンが押せなくなるGitLabの機能: ちなみにこの機能自体はGitLabの方がGitHubよりも前から搭載していて、後から追いかけたGitHubではdraft pull requestという名称なんですよ」「へぇ〜!」

参考: Draft Pull Requestをリリースしました - GitHubブログ

「WIPという略語よりdraftの方が非英語話者とかにもわかりやすいからとも書かれてますね」「WIPとかLGTMって何の略かもあまり考えずに使ってたかも」「たしかに略語だと通じる範囲が狭まるので、ちょっとわかる」

「GitLab OAuthのimplicit grantも非推奨化: 今は明示的にやるのが普通なのであまりやらなさそう」「CI_PROJECT_CONFIG_PATHCI_CONFIG_PATHに変わる」

参考: GitLab as an OAuth2 provider | GitLab

「期限切れのsshキーを追加するとデフォルトで無効にするようになった」「GitLab 13.9でsshキーを管理者が強制的に期限切れにするオプションが入っていたのね」「うっかりするとCIが止まったりして」

参考: Optional enforcement of SSH key expiration (#250480) · Issues · GitLab.org / GitLab · GitLab

「Code Quality?」「あぁ、Code QualityはGitLabの機能名で↓、そこでサポートするデフォルトのRuboCopバージョンを変更したのね」「Ruby 2.4〜3.0をサポートして2.1〜2.3のサポートは終了するけど、コンフィグで引き続きサポート可能なところがさすがのGitLabですね👍」

参考: Code Quality | GitLab

「最近のGitLabはメジャーバージョンアップを以前よりも頻繁にするポリシーになっているんですけど、今回のGitLab 14.0はbreaking changesが割とあるので、後でじっくりチェックしておこうっと」

🔗 fx: Railsで使うPostgreSQLの関数やトリガーを管理

teoljungberg/fx - GitHub

以下の記事で知りました。

参考: Logidze 1.0: Active Record, Postgres, Rails, and time travel — Martian Chronicles, Evil Martians’ team blog


つっつきボイス:「関数だからfxなのかな」「PostgreSQLの関数やトリガーを別ファイルに書いてマイグレーションで管理するようですね」「マイグレーションにdrop_functiondrop_triggerも書けるらしい」

# 同リポジトリより
% rails generate fx:function uppercase_users_name
      create  db/functions/uppercase_users_name_v01.sql
      create  db/migrate/[TIMESTAMP]_create_function_uppercase_users_name.rb
# 同リポジトリより
def change
  drop_function :uppercase_users_name, revert_to_version: 2
end

「この書き方どこかで見たな、たしかデータベースVIEWを扱えるgem...scenicだ」「あ、たしかに」「scenicはSQLファイルを別途作ってそこに生SQLを書いて使うんですけど、このあたりとかfxととても似てる↓」「ホントだ」

# scenic-views/scenicより
$ rails generate scenic:view search_results
      create  db/views/search_results_v01.sql
      create  db/migrate/[TIMESTAMP]_create_search_results.rb

scenic-views/scenic - GitHub

「インターフェイスがこんなに似てるということは、fxとscenicは同じ人が作ってるのかな?: コントリビュータを見ると、fxの作者もscenicにコミットしてる↓」「なるほど納得」「どちらのgemもやっていることは似ているので、fxの機能がscenicに入ったらいいかも」

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

🔗 DatabaseCleaner設定の見直し


つっつきボイス:「truncationdeletionに変えて速くなる場合がある、なるほど」

DatabaseCleaner/database_cleaner - GitHub

「DatabaseCleanerは使いこなしが大変」「システムテストだとDatabaseCleanerがなくてもよくなったんでしたっけ?」「最初からDatabaseCleanerなしで動くようにテストが書かれていればいいんですけど、データベース書き込みをテストするようになると何らかの形でrewinder的なものが必要になって、気をつけないとテストの実行順序で結果が変わったりすることもあるんですよ」「あ〜」「テスト数が多くなると原因を特定するだけでも時間がかかるので、DatabaseCleanerを入れて様子を見たりしましたよ」

Rails: システムテストをRSpecで実行する(翻訳)

🔗 Railsのセキュリティ脅威を学ぶ: 認証編


つっつきボイス:「ざっと見た感じでは、項目ごとに具体的なコードもあって丁寧に書かれていそうですね👍」「お〜!」「この記事には他のシリーズもあるみたいですね↓: OWASPトップテンと同じ見出しなので今後もトップテンを順に追いかけて記事にしていくみたい」「なるほど」「これ全部追いかけたら凄い」「この記事翻訳したいです」

参考: Rails Security Threats: Injections - Honeybadger Developer Blog

  • Injection
  • Broken authentication(上の記事)
  • Sensitive data exposure
  • XML external entities (XXE)
  • Broken access control
  • Security misconfigurations
  • Cross-site scripting (XSS)
  • Insecure deserialization
  • Using components with known vulnerabilities
  • Insufficient logging and monitoring
    シリーズ見出しより

「ちなみに記事の冒頭にあるOWASP(Open Web Application Security Project)はこういうセキュリティ上の脅威トップテンみたいなものを定期的に発表しています↓」


前編は以上です。

バックナンバー(2021年度第2四半期)

週刊Railsウォッチ: childprocess gemで子プロセスを制御、Ruby 2.6〜3.0で動くdelegationほか(20210623後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。