更新日:2026年10月6日

6分で読めます

Django製GRCツールにモジュール単位のアクセス制御を実装する

GitLabのセキュリティコンプライアンスチームと内部監査チームという性質の異なる2つのチームを1つの基盤で支えるために、Django製の社内ツールをどう再構築したのか。その技術的な詳細を解説します。

GitLabのエンジニアリングチームは、数多くの社内ツールを自前で開発しています。なかでも、認可の扱い方そのものを見直すきっかけになったのが、GRC(ガバナンス・リスク・コンプライアンス)の社内ツールでした。このツールは、セキュリティコンプライアンスチームと内部監査チームという、性質の大きく異なる2つのユーザー層を1つの基盤で支えています。

複数のオーディエンス、異なるデータ分類レベル、互いに混ざってはいけないデータ。こうした条件を抱えたDjango製の社内ツールを開発・運用しているなら、本記事が参考になるはずです。私たちが直面した課題と、その解決方法、そして同じパターンをご自身のアプリに適用する手順を順に紹介します。

課題

もともとのツールは、セキュリティコンプライアンスチーム専用に作られた単一のDjangoアプリで、すべてのビューにlogin_requiredを付けていました。この設計は当初、正しいものでした。しかし、内部監査チームがこのツールに新しいユースケースを見いだしたことで前提が変わります。そのユースケースには、完全に独立したワークフロー、独自のデータ、独自のモデル、そしてセキュリティコンプライアンスチームには見せられない機密情報が含まれていたのです。

login_requiredが答えてくれるのは「ログインしているか」という1点だけ。マルチテナントの社内ツールで本当に問わなければならないのは「ここにアクセスしてよい人物か」という別の問いです。認証済みのセキュリティコンプライアンスチームのユーザーが内部監査チームのURLを開けば、ログインチェックを素通りして、本来見るべきではないデータに到達してしまいます。

後からもう1つのオーディエンスが増えたアプリであれば、思い当たる節があるかもしれません。ここで自問していただきたいのは、きわめてシンプルな問いです。アプリ内のすべてのビューは、ユーザーが「誰か」を確認しているでしょうか。それとも「ログインしているか」を確認しているだけでしょうか。後者であれば、私たちと同じ穴が空いている可能性が高いと言えます。

必要だったのは、きれいな分離でした。「このボタンには触れないでください」という運用上のお願いではなく、どのURLを直接入力されても崩れない、強制力のある境界です。

アーキテクチャの判断

内部監査チームのユースケースがロードマップに乗ったとき、典型的な分岐点が目の前に現れました。既存のアプリに機能を継ぎ足すか、構造から作り直すか。私たちは作り直す道を選びます。

できあがったのは、コアとなるDjangoプロジェクトの上に、独立した2つのモジュールアプリが乗る構成です。コアが持つのは、共通の認証、ユーザー管理、監査ログ、そして基本UIコンポーネント。その上にセキュリティコンプライアンス向けと内部監査向けのモジュールが1つずつ並び、各モジュールが自身のモデル、ビュー、API面を所有します。

そして、自分たちに課した制約こそが、この取り組みを記事にするに値する核心でした。コアはコンプライアンスのデータについても内部監査のデータについても一切知らない、という制約です。どちらのモジュールもコアに依存してよい。しかし、モジュール同士が依存し合うことは認めない。この一線があるからこそ、2つのアプリを場当たり的に継ぎはぎしただけの構造に陥らずに済みます。

アクセス制御

私たちは、機密性の高いコンプライアンスデータとリスクデータを保持するツールを開発する、コンプライアンスエンジニアリングの部門です。ここでアクセス制御が甘ければ、それは単なるバグでは済みません。信頼そのものを損なう重大な問題になります。

設計上の判断として選んだのは、Djangoに最初から備わっている認証システムでした。各モジュールは、ちょうど1つのグループに対応します。ユーザーは片方のグループだけに属することも、両方に属することも、どちらにも属さないことも可能で、どの組み合わせであってもシステムが適切な職務分離を各レイヤーで担保します。ナビゲーションUI、Djangoのビュー、REST APIが、それぞれ独立してグループメンバーシップを検証する仕組みです。

実装

コアには、両モジュールが継承する再利用可能なクラスが1つだけあります。GroupRequiredMixinです。このミックスインがグループメンバーシップを確認し、ユーザーが必要なグループに属していなければPermissionDeniedを送出します。

モジュール単位のサブクラス

各モジュールが用意するのは、required_group_nameを設定するだけの3行のサブクラスです。セキュリティコンプライアンス側にはGasGroupRequiredMixinとIsGasGroupMemberがあり、内部監査側も同じパターンを繰り返します。モジュールに強制レイヤーを追加するコストは、これで全部。新しいファイルが1つ、新しい属性が1つだけ。実際のチェックロジックはコアに置かれ、以後そこに手を入れる必要はありません。

UIレイヤー

最後に、テンプレートフィルターuser_in_groupを用意しました。ユーザーが実際に閲覧を許可されているものだけをナビゲーションに表示させるためです。ERMメンバーだけに見せたいナビゲーションブロックを、user|user_in_group:'erm'のようなチェックで囲みます。

ただし、これはUX上のガードであって、セキュリティ境界ではありません。ナビゲーションのリンクを隠したところで、URLを直接入力する行為は止められないからです。それを防ぐのがミックスインとDjango REST Frameworkのパーミッションクラスの役割であり、テンプレートフィルターは「そのユーザーに何が見えて何ができるのか」をUIの側で正直に保つためのものと言えます。

今後の展望とはじめの一歩

プラットフォームのレベルまで視点を引き上げると、この成果がモジュールにもチームにも依存しない再利用可能なパターンであることが見えてきます。仮に3つ目のモジュール、たとえばベンダーリスクのワークフローや監査管理モジュールなどが加わったとしても、同じアクセス制御の構造、同じ監査証跡、同じ共有認証をそのまま継承でき、強制ロジックを1行も新たに書く必要がありません。新しいモジュールのコストは、インフラの足場作りではなく、ほぼビジネス要件だけに収まります。

これが私たちの選んだ賭けでした。土台は一度だけ作り込み、以降のモジュールはすべてその上に乗せる。アクセス制御をゼロから作り直す作業は、もう繰り返しません。

同じ仕組みをご自身のアプリで試すなら、おすすめの順序は次のとおりです。

  1. ビューに「ログイン確認」と「認可」のギャップがないかを点検します。login_requiredとLoginRequiredMixinをgrepし、それぞれについて、匿名ユーザーだけでなく他の認証済みユーザーからも機密データを守れているかを確認してください。
  2. コードを書く前にモジュール境界を引きます。本当に共有すべきもの(認証、基本UI、ロギング)と、特定のオーディエンスだけに属するものを切り分けてください。
  3. ビューごとではなく、ミックスインを1つだけ書きます。コアアプリに置いた単一のGroupRequiredMixinをモジュールごとにサブクラス化すれば、強制ロジックをレビューすべき箇所も、誤りが生じうる箇所も、それぞれ1か所に収まります。
  4. ビュー、API、ナビゲーションの各レイヤーで独立して強制します。リンクを隠すことが唯一の防衛線になる状況を避けるためです。

参考リンク

ご意見をお寄せください

このブログ記事を楽しんでいただけましたか?ご質問やフィードバックがあればお知らせください。GitLabコミュニティフォーラムで新しいトピックを作成してあなたの声を届けましょう。

フィードバックを共有する

今すぐ開発をスピードアップ

DevSecOpsに特化したインテリジェントオーケストレーションプラットフォームで実現できることをご確認ください。