0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

📝pytestを「事故防止台帳」として育てる 第3段階:51件から69件へ、認証・CSRF・アクセス制御を強化した記録

0
Last updated at Posted at 2026-08-11

pytest_No3.png

pytest強化シリーズ


はじめに

第3段階は、認証・CSRF・アクセス制御まで扱ったことで、かなりボリュームの大きい記事になりました。

そのままだとスクロール量がかなり多くなるため、読みやすいように一部の詳細内容を<details>タグで折りたたんでいます。

本文だけでも流れは追えるようにしているので、気になるところだけ開いて読んでもらえればと思います。

私は現在、本業でトラックドライバーとして働きながら、Pythonを中心にWebアプリケーション開発を独学しています。

個人開発中のFlaskアプリでは、pytestを単なる「動作確認」ではなく、

一度見つけた事故やヒヤリハットを、次から自動で止めるための事故防止台帳

として少しずつ育てています。

これまで、

  • 第1段階:3件 → 9件
  • 第2段階:9件 → 51件

まで強化してきました。

今回の第3段階では、

認証・CSRF・アクセス制御

を中心に見直し、

51 passed
↓
69 passed

まで回帰テストを増やしました。

ただし、今回もテスト数そのものを増やすことが目的ではありません。

第3段階で確認したかったのは、

誰でもデータを変更できないか

ログインしていても、
外部サイトなどから意図しないPOSTを送られないか

業務画面やAPIへ匿名で入れないか

匿名ユーザーからAI APIを実行されないか

といった部分です。

今回もCodexと一緒に、

調査
↓
RED
↓
最小修正
↓
GREEN

という順番で進めました。


第3段階開始時は51件

第2段階終了時点では、

51 passed

でした。

すでに、

  • 売上入力validation
  • 商品登録validation
  • 不正な商品ID
  • 月違いの商品
  • 販売終了商品
  • DB一意制約
  • commit失敗時のrollback
  • 論理削除と売上履歴保持
  • dashboard集計

などはpytestで確認していました。

一方で、認証まわりを調査すると、

認証
CSRF
アクセス制御

はまだ十分に実装されていませんでした。

そこで第3段階では、

認証
↓
CSRF
↓
業務画面・APIのアクセス制御

の順番で進めることにしました。


まずは修正せず、現在状態を調査した

最初にCodexへ依頼したのは実装ではありません。

現在の認証状態を調査するだけ

にしました。

調査時点では、

  • ログイン画面なし
  • Userモデルなし
  • Flask-Loginなし
  • CSRF tokenなし
  • 業務画面/APIも匿名アクセス可能

という状態でした。

特に状態を変更するPOSTでは、

POST /
POST /input

へ未認証でも到達できました。

つまり、

ログインしていない利用者でも、商品や売上データの変更処理へ到達できる状態

でした。

ここでいきなり修正せず、

まずその危険な状態をpytestで表すことにしました。


認証:未認証POSTをREDにする

最初に追加したのは、

未認証の商品POSTを拒否する
未認証の売上POSTを拒否する

というテストです。

期待する将来仕様は、

anonymous
↓
POST
↓
302
↓
/login
↓
DB変更なし

です。

しかし実装前は、

期待:302
実際:200

となり、

実際にDB変更処理まで到達しました。

狙いどおりREDです。

🧪 認証REDテストから実装までの詳細を見る

ログイン仕様も先にREDで決めた

次に、

  • GET /loginが表示できる
  • 正しい認証情報ならログインできる
  • 間違ったpasswordではログインできない
  • ログイン状態が同じclientで維持される
  • 未認証POSTは拒否される

という仕様をテストへ追加しました。

この時点ではログイン機能そのものが存在しないため、

5 failed

になりました。

ここで初めて認証実装へ進みました。

単一管理者方式を採用した

今回のアプリでは、いきなり複数ユーザー管理までは広げませんでした。

採用したのは、

単一管理者ログイン

です。

認証にはFlask-Loginを使用しました。

認証情報は、

SECRET_KEY
ADMIN_USERNAME
ADMIN_PASSWORD_HASH

を環境変数から取得します。

本番用の平文passwordをコードへ書くのではなく、

check_password_hash()

でpassword hashを検証する形にしました。

この段階では、

  • User DBモデル
  • 一般ユーザー
  • role
  • tenant
  • 店舗別権限

などは追加していません。

今回の仕様では、

認証済み利用者
=
単一管理者

として扱っています。

そのため今回の第3段階では、複数のroleによる詳細な認可ではなく、

「認証済み管理者だけが業務機能へアクセスできる」

というアクセス制御までを対象にしました。

認証実装後は56件GREEN

単一管理者認証を実装すると、

pytest test_auth.py -v
→ 5 passed

pytest -v
→ 56 passed

となりました。

ここで一度、

feat: add single-admin authentication

としてローカルcommitを作りました。

認証だけがGREENになった地点を残し、

そのままCSRFまで一気に進めないようにしました。


次はCSRFを調べた

認証を追加すると、

「ログイン済みだから安全」

とは限りません。

Session Cookieを使った認証では、

ログイン状態を利用して、利用者が意図していないPOSTを送られる

可能性も考える必要があります。

そこで次はCSRF対策へ進みました。


CSRFも実装前にREDを作った

今回作ったCSRFテストは6件です。

実装前は、

6 failed, 56 passed

でした。

フォームにはtokenが存在せず、

認証済み状態ならtokenなしの商品・売上POSTも通りました。

さらにtokenなしログインでも、

正しいusernameとpasswordならログインできていました。

🧪 CSRFの6件のREDテストと実装内容を見る

追加したCSRFテスト

ログインフォームにCSRF tokenがある

商品フォームにCSRF tokenがある

売上フォームにCSRF tokenがある

tokenなしログインPOSTを拒否する

tokenなし商品POSTを拒否する

tokenなし売上POSTを拒否する

tokenなしでもDB変更まで進んでいた

CSRF実装前の商品POSTでは、

status=200
products_unchanged=False

となりました。

つまり、

CSRF tokenがなくてもProduct変更処理へ到達していた

ということです。

売上POSTでも、

status=200
sales_unchanged=False

となり、

DailySalesが変更されました。

単に、

CSRF対策がない

というコード上の指摘だけではなく、

現在の状態では実際にどこまで処理が進むのか

をREDテストで確認しました。

Flask-WTFでCSRFProtectを追加

CSRF対策にはFlask-WTFを使いました。

CSRFProtect(app)

で保護を有効化し、

3つのPOSTフォームへ、

<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

を追加しました。

対象は、

/login
/
/input

です。

CSRF exemptは追加せず、

テスト環境でもCSRF保護を無効化していません。

テスト側も「正規のPOST」に変更した

CSRFProtectを有効にすると、

既存の正常系テストもtokenなしでは400になります。

しかし、

商品validationを確認したいテスト

売上rollbackを確認したいテスト

までCSRFで止まってしまうと、本来確認したい業務処理へ到達できません。

そこでテスト側も、

GET
↓
csrf_token取得
↓
有効token付きPOST

という形へ変更しました。

CSRF tokenの取得には、テスト用fixtureを使っています。

認証済みclientも、

Sessionへ直接ログイン状態を書き込むのではなく、

GET /login
↓
CSRF token取得
↓
username
password
csrf_token
↓
POST /login
↓
認証済みclient

という実際のログインに近い流れで作っています。

CSRF実装後は62件GREEN

実装後は、

pytest test_csrf.py -v
→ 6 passed

pytest test_auth.py -v
→ 5 passed

pytest -v
→ 62 passed

となりました。

tokenなしの商品・売上POSTでは、

CSRFProtectによって400で拒否
↓
DB変更なし

となることを確認しています。

商品POSTではProductとDailySales、

売上POSTではDailySalesについて、

POST前後のsnapshotが変わらないことをテストしました。

tokenなしログインについても、

400
↓
認証状態が作られない

ことを確認しています。

ここでも一度、

feat: add CSRF protection

としてcommitしました。


認証とCSRFを入れても、まだ匿名で業務画面へ入れた

認証とCSRFを入れたあと、

次はGETルートとAPIを調査しました。

対象にしたのは、

/login
/
/input
/dashboard
/api/dashboard-data
/api/ai-advice
/api/greeting

です。

調査時点では、すべて匿名アクセスで、

HTTP 200

となりました。

/loginは公開で問題ありません。

しかし、それ以外の業務画面やAPIも匿名で利用できる状態でした。


匿名でもAI APIまで実行できた

特に気になったのは、

/api/ai-advice
/api/greeting

です。

調査時には実Gemini APIへ接続せず、

Gemini Clientをモックしました。

その結果、匿名アクセスでも、

Gemini Client
→ 1回呼び出し

となりました。

つまり条件が揃えば、

ログインしていない利用者でもAI処理へ到達できる

状態でした。


アクセス制御もREDから始めた

そこで新しく、

test_authorization.py

を追加しました。

ファイル名にはauthorizationを使っていますが、

今回確認しているのはroleごとの詳細な認可ではなく、

匿名ユーザーから業務画面・APIを保護するアクセス制御

です。

今回の将来仕様は、

GET /login
→ anonymous 200

それ以外の業務画面/API
→ anonymous 302
→ /login

としました。

最初の結果は、

1 passed, 6 failed

でした。

公開する/loginだけGREEN。

残り6ルートはすべて匿名へ200を返していたためREDです。

🔐 アクセス制御の実装と既存テスト調整の詳細を見る

「既存テスト変更が必要」でCodexが停止した

ここで少し印象に残った出来事がありました。

アクセス制御を実装しようとしたところ、

Codexが、

既存テストの変更が必要になる

と判断して実装前に停止しました。

例えば、

test_dashboard.py

は匿名clientでAPIの集計結果を確認していました。

しかしAPIを認証必須にすると、

匿名clientでは302になり、

集計処理そのものへ到達できません。

同じようにCSRFテストにも、

匿名状態で業務画面からtokenを取得している箇所がありました。

そこで、

test_authorization.py
→ 匿名で入れないことを確認

test_dashboard.py
→ 認証済みなら正しく集計できることを確認

test_csrf.py
→ CSRFそのものを確認

test_auth.py
→ 認証そのものを確認

と、テストの役割を整理しました。

Codexには、

想定外の変更が必要なら勝手に進めず、一度報告する

というルールを入れていました。

今回はそのルールどおり、

既存テストを勝手に変更せず停止したため、

変更理由を確認してから最小限の修正を許可しました。

login_requiredで6ルートを保護した

最終的に、

/
/input
/dashboard
/api/dashboard-data
/api/ai-advice
/api/greeting

login_requiredを追加しました。

実装後は、

GET /login
→ anonymous 200

その他6ルート
→ anonymous 302
→ /login

となりました。

AI APIについても、

匿名アクセス時には、

Gemini Client
→ 0回

となることを回帰テストで確認しています。

つまり、匿名ユーザーはAI処理本体へ到達しません。


第3段階の最終結果は69件GREEN

最終的に、

51 passed
↓
56 passed
↓
62 passed
↓
69 passed

となりました。

第3段階で直接追加した、

認証           5件
CSRF           6件
アクセス制御   7件

の合計18件が増え、

51
↓
69

となりました。

✅ pytestの実行結果と各テストの役割を見る

最終pytest結果

pytest test_authorization.py -v
→ 7 passed

pytest test_auth.py -v
→ 5 passed

pytest test_csrf.py -v
→ 6 passed

pytest test_dashboard.py -v
→ 4 passed

pytest -v
→ 69 passed

テスト数より「担当」が分かれてきた

第3段階で特に感じたのは、

単にテスト数が増えたというより、

テストごとの担当がかなり分かれてきた

ことです。

test_auth.py
→ ログイン・未認証POST拒否

test_csrf.py
→ CSRF tokenとtokenなしPOST拒否

test_authorization.py
→ 匿名から業務画面/APIへ入れないこと

test_dashboard.py
→ 認証後の集計処理

test_products.py
→ 認証・CSRF通過後の商品処理

test_sales.py
→ 認証・CSRF通過後の売上処理

以前は、

とりあえずレスポンスが返る

くらいの確認だったpytestが、

少しずつ、

「どの事故を、どのテストが止めるのか」

分かる状態になってきました。


feature branchとPull Requestも使った

今回の第3段階では、

feature/auth-hardening

というfeature branchで作業しました。

ローカルで69件GREENを確認したあと、

feature/auth-hardening
↓
GitHubへpush
↓
Pull Request
↓
GitHub Actions
↓
mainへMerge

という流れを通しました。

これまでローカルでのpytestとGitHub Actionsは使っていましたが、

今回はfeature branchからPull Requestを作り、CIの成功を確認してからmainへ入れるところまで進めました。

🚚 commit・Pull Request・GitHub Actionsの詳細を見る

commitも工程ごとに分けた

feat: add single-admin authentication

feat: add CSRF protection

feat: protect authenticated routes

と分けました。

Pull Requestでは、

  • baseがmain
  • compareがfeature/auth-hardening
  • 3commit
  • 変更差分
  • conflictの有無
  • GitHub Actionsの結果

を確認してからMergeしました。

GitHub Actionsでも69件GREEN

Pull Request作成後、

GitHub ActionsのRun Testsが実行されました。

CI上でも、

collected 69 items

となり、

最終結果は、

69 passed

でした。

GitHub Actionsの結果も、

Success

となったことを確認してからmainへMergeしました。

つまり今回の第3段階では、

ローカルpytest
69 passed
↓
feature branchへpush
↓
Pull Request
↓
GitHub Actions
69 passed
↓
mainへMerge

という流れまで確認できました。

Merge後のmainでも69件確認

Pull RequestをMergeしたあと、

ローカル環境もmainへ戻し、

origin/mainと同期しました。

Local mainとorigin/mainのHEADが一致していることを確認したあと、

もう一度、

pytest -v

を実行しました。

結果は、

69 passed

でした。

作業ツリーもクリーンな状態です。

ここまで確認して、第3段階を完了扱いにしました。


私の中ではPRも「出荷判定」

以前からpytestを、

出庫前点検

GitHub Actionsを、

出荷ゲートでの自動点検

のように考えていました。

今回Pull Requestを使ったことで、

その間にもう一つ、

作業用branch
↓
Pull Request
↓
CI
↓
main

という確認工程が入りました。

トラックの仕事に例えるなら、

作業場で点検
↓
出荷判定
↓
自動検査
↓
本線へ流す

ようなイメージです。

ローカルで動いたからすぐmainへ入れるのではなく、

一度別の場所で差分とテスト結果を確認してからmainへ入れる

という流れが、実際に使ってみて少し分かりました。


第3段階終了時点

第3段階終了時点では、

単一管理者認証
✅

CSRF
✅

業務画面/APIの匿名アクセス拒否
✅

匿名AI API実行防止
✅

pytest
69 passed

GitHub Actions
69 passed / Success

Pull Request
✅

mainへMerge
✅

Merge後のmain
69 passed

となりました。

今回決めた範囲では、

第3段階完了

です。


あえて今回やらなかったこと

第3段階の最終点検では、

「見つけたけれど、今回は直さない」

と決めた項目も残りました。

ここまで全部手を広げると第3段階のゴールが曖昧になるため、

今回の範囲は、

単一管理者認証
CSRF
業務画面/APIの匿名アクセス拒否

までとしました。

📋 今回あえて後回しにした改善候補を見る

最終点検で確認した改善候補は、

logout
設定不足時のfail-closed専用pytest
不正CSRF token専用テスト
Session Cookie設定
ログイン試行rate limit
APIの401 JSON化
認証チェック重複の整理

などです。

logoutはまだ実装していない

現在は、

logout route
logout_user()
ログアウトボタン

はまだありません。

匿名アクセス防止そのものは成立していますが、

Session Cookieを利用する認証方式なので、

管理者が任意のタイミングでSessionを終了できるlogoutは、

今後追加したい機能です。

特に共有端末で運用する場合には、

本番公開前に必要性を改めて判断する予定です。

roleによる詳細な認可はまだない

今回のアプリには、

  • User DBモデル
  • 一般ユーザー
  • role
  • tenant
  • 店舗ID
  • 店舗別アクセス制御

はありません。

現在ログインできるのは、

環境変数で設定した単一管理者だけです。

そのため今回の第3段階では、

認証済み利用者
=
管理者

という前提で、

認証済み管理者だけが業務機能へアクセスできる状態

までを完了条件としました。

将来複数ユーザーを導入する場合は、

roleや店舗単位の権限設計を別途考える必要があります。

本番運用前には環境変数が必要

今回の認証機能では、

SECRET_KEY
ADMIN_USERNAME
ADMIN_PASSWORD_HASH

を環境変数から取得しています。

そのため、本番環境でログイン機能を利用するには、

これらを正しく設定する必要があります。

特に、

ADMIN_PASSWORD_HASH

には平文passwordではなく、

Werkzeug互換のpassword hashを設定する前提です。

今回の第3段階では、

Render側の本番環境設定までは変更していません。


まとめ

pytest強化の第3段階では、

51 passed
↓
69 passed

となりました。

今回追加した中心部分は、

  • 単一管理者認証
  • CSRF保護
  • 業務画面/APIのアクセス制御
  • 匿名AI API実行防止
  • 認証・CSRF・アクセス制御の回帰テスト

です。

第1段階では、

3件しかなかったpytestを回帰テストとして育て始める

ところから始まりました。

第2段階では、

DB更新やrollback、論理削除、集計処理

まで広がりました。

そして第3段階では、

「正しいデータを保存できるか」だけでなく、「誰がその処理へ到達できるか」

までテスト対象が広がりました。

さらに今回は、

feature branch
↓
Pull Request
↓
GitHub Actions
↓
mainへMerge

という流れも初めて実際に使いました。

まだ改善できる部分はあります。

それでも、一つ問題を見つけるたびに、

調査
↓
RED
↓
最小修正
↓
GREEN
↓
commit
↓
次の問題

と進めることで、

pytestが少しずつ、

事故が起きたあとに確認するものではなく、事故を入口で止める仕組み

になってきたと感じています。

次は第4段階へ進む予定です。

pytest強化シリーズ


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?