CUEとコード生成で実現する宣言的なアクセス制御

ogp

enechainのGXデスク バックエンドエンジニアの @eji です。

はじめに

GXデスクで開発している環境価値取引所JCEXでは、「契約プラン×ロール」という二軸の組み合わせでアクセス制御を管理しています。 画面の中でもこの組み合わせに応じて操作できる機能を制御しており、条件にAND/ORが絡むため管理は複雑になっていました。

本記事では、この複雑さを解決するために開発したCLIツール lattice について紹介します。 プランとロールの組み合わせが格子(lattice)状になるイメージから名付けました。

latticeは、CUE言語でアクセス制御ルールを宣言的に定義し、TypeScript/Goのアクセス制御関数とドキュメントを自動生成するツールです。 ここでいう「型安全」とは、主にプラン名・ロール名・リソースIDの参照整合性を、定義時と生成コードの型で検証できることを指しています。

対象読者

  • 複雑なアクセス制御ロジックを管理している方
  • アクセス制御ルールの散在に課題を感じている方
  • CUE言語やコード生成に興味がある方

背景:JCEXにおけるアクセス制御の課題

プラン×ロールのアクセス制御モデル

JCEXでは、ユーザーが操作できる機能は契約プランユーザーロールの組み合わせで決まります。 プロダクトの成長や取り扱い商品の拡大に伴い、プランとロールの組み合わせが増え、アクセス制御モデルは徐々に複雑化していきました。

Problem

  • プラン: 組織に割り当てられる契約単位。契約に応じて付与され、1つの組織が複数のプランを持つことがある
  • ロール: 個人に割り当てられる操作権限。同じ組織内でもユーザーごとに異なるロールを持つ

たとえば以下のようなアクセス制御条件があります(プラン名・ロール名・条件の組み合わせは説明用の架空の例です)。 ここでは standard / premium / enterprise がプラン名、editor / admin がロール名です。

リソース プラン条件 ロール条件
market:read standard or premium or enterprise
market:write premium or enterprise editor or admin
report:read enterprise
report:read standard or premium editor

report:read は2行で1つのリソースを表しており、いずれかの行を満たせばアクセスできます。 このように、リソースごとにプランやロールの条件が微妙に異なるため、リソースが増えるほど管理が煩雑になっていました。

実装上の課題

この複雑なアクセス制御モデルに対して、実装上で以下の課題が発生していました。

  • 手続き的な表示制御ロジックによる論理的ミス: フロントエンドではプラン×ロールの組み合わせをif文で記述していたため、条件分岐の抜け漏れによるバグが発生していた
  • アクセス制御設定の全体像が把握困難: 非エンジニアを含め「どのプラン×ロールで何が使えるのか」を一覧できず、仕様確認のたびにコードを読む必要があった
  • フロントエンドとバックエンドの二重管理: フロントエンドでは表示制御に、バックエンドでは認可にそれぞれプランとロールが利用されており、書き方が統一されていなかった

検討した解決案

これらの課題を解決するために、以下の選択肢を検討しました。

  • TypeScriptで宣言的にアクセス制御の設定が書けるフレームワークを用意
  • 設定ファイルからのコード生成

latticeを作った経緯は、フロントエンドの表示制御が手続き的になっていてバグが発生しやすい構成を改善するためでした。 そのため、TypeScriptで宣言的に書ければ十分と考えていました。 しかし、将来的にバックエンドへの導入も視野に入れると、TypeScriptに依存しない形でアクセス制御ルールを定義する方が望ましいと判断しました。

設定ファイルからのコード生成であれば、フロントエンド(TypeScript)とバックエンド(Go)の両方でアクセス制御ルールを定義できるため、この方法が良いだろうと判断しました。

なぜCUEを選んだのか

検討した選択肢

設定ファイルの定義言語として当初はJSONまたはYAMLを検討しました。

JSON Schemaを使えばバリデーションやIDEでの補完が可能で、プランやロールも enum で制約できます。 しかし、any / all を含むアクセス制御条件をJSON Schemaで表現するとスキーマが冗長になりやすく、読みやすさと保守性を保つのが難しいという課題がありました。

CUEの場合は比較的シンプルにスキーマを定義でき、スキーマを変更しなくても設定した値により制約を加えることができます。 JCEXで取り扱っている商品毎にプランが増えることもあり、今後のメンテナンス性を考慮してCUEを採用しました。

CUEの利点

CUEは、設定やスキーマの定義に特化した言語です。CUEには「型と値が同じ概念(制約)である」という特徴があります。 つまり、設定ファイル内で定義した値を、型の制約として利用できるということです。

具体的には、latticeでは以下のようなCUEスキーマを定義しています。

CUEスキーマ

allow の各要素は、plans または roles の少なくともどちらか一方を指定する前提で利用しています(空オブジェクトは意図しない定義のため、lattice validate の論理チェックで検出対象としています)。

ここで注目すべきは #ValidPlan#ValidRole の定義です。

#ValidPlan: or([for p in plans {p}])

これは「plans リストで定義されたプラン名のいずれか」という制約を表現しています。 この制約があるため、resources 内のプラン名はトップレベルの plans リストで定義されたもののみ指定できます。

これにより、存在しないプラン名を誤って書いた場合、CUEの評価時点でエラーになるため、typoや定義漏れをコード生成の前に検知できます。

CUEはCLIツールとGoライブラリの両方で利用できるため、コード生成の入力定義に適していました。 latticeではライブラリ(cuelang.org/go)を利用して、CUE定義のパースとバリデーションを行い、定義内容に基づいてコードとドキュメントを生成しています。

latticeの設計

全体の流れ

latticeは以下の4つのコマンドを提供します。

コマンド 役割
lattice init プロジェクトの初期化(cue.mod とサンプル policy.cue を生成)
lattice validate ポリシーファイルの構文・スキーマ整合性を検証し、プラン名/ロール名の参照ミスを検出
lattice code TypeScript/Goのアクセス制御関数を生成
lattice doc Markdown/HTMLのドキュメントを生成

CUEでのアクセス制御ルール定義

JCEXでは、プラン・ロールの一覧を definitions.cue に、リソースごとのアクセス制御条件を policy.cue に分けて定義しています。以下はJCEXの実際の構造に基づいた例です(プラン名・ロール名は架空のものに置き換えています)。

まず、プラン・ロールの定義です。

definition

リソースごとのアクセス制御条件は、以下のルールで評価されます。

ルール 意味 記述例
複数 allow 条件同士は OR allow: [条件1, 条件2]
plans + roles 軸同士は AND {plans: プラン条件, roles: ロール条件}
軸の省略 省略した軸は不問 {plans: プラン条件} はロール不問
any 候補内は OR {plans: {any: [A, B]}}
all 候補内は AND {plans: {all: [A, B]}}
any + all 同一軸内で AND {plans: {all: [A, B], any: [C, D]}}

これらのルールを使った定義例です。

Sample

lattice.#Policy & でスキーマを適用し、定義内容をバリデーションしています。

CUEのマルチファイルパッケージの仕組みにより、同じディレクトリ内の definitions.cuepolicy.cue は1つのパッケージとして読み込まれるため、_plans_roles をそのまま参照できます。

このため、コマンド例で -i policy.cue を指定した場合でも、同一ディレクトリの同一パッケージに含まれる definitions.cue もあわせて評価されます。

market:read はプランのみ、market:write はプラン×ロールのAND、report:readallow を複数書くことでOR条件を表現しています。 背景で紹介した3つのアクセス制御条件がそれぞれCUEの定義に対応していることが確認できます。

生成されるコード

上記のCUE定義から lattice code コマンドを実行すると、TypeScriptまたはGoのアクセス制御関数が生成されます。JCEXでは現在TypeScriptのコードを生成しています。

cli

generated code

Plan, Role, Resource がユニオン型として定義されるため、存在しないプラン名やロール名を指定するとコンパイルエラーになります。

なお、latticeはGoのコード生成もサポートしています(--lang go)。バックエンドへの導入については後述するマルチポリシー対応で触れます。

ドキュメント生成

latticeはアクセス制御関数だけでなく、Markdown/HTMLのドキュメントも生成します。

doc

生成されるMarkdownドキュメントには、プラン・ロールの一覧と、リソースごとのアクセス制御条件が表形式でまとめられます。 たとえば report:read リソースの条件は次のように出力されます。

markdowndoc

「どのプラン×ロールで何が使えるのか」を、コードを読まなくても確認できます。 コードとドキュメントは同じCUE定義から生成されるため、コードとドキュメントの乖離を起こしにくい設計にできます。

マルチポリシー対応

JCEXでは現在フロントエンド向けに単一のポリシーを生成していますが、latticeはマルチポリシーもサポートしています。

たとえば、フロントエンドとバックエンドでアクセス制御対象のリソースが異なる場合、共通のプラン・ロール定義を1つのファイルに集約し、レイヤーごとのポリシーを別ファイルに分離できます。

frontend policy

backend policy

--policy フラグで生成対象を切り替えます。

cli sample

definitions.cue のプラン・ロール定義は共有しつつ、リソースセットはレイヤーごとに分離できるため、それぞれの関心に応じたコードを生成できます。

開発プロセス

latticeの開発はClaude Codeを使って進めました。

まずCLIのインターフェースを設計し、どのようなコマンド(init / validate / code / doc)があると嬉しいかを整理しました。 次に、各コマンドの入力(CUE定義ファイル)と出力(TypeScript/Goコード、Markdownドキュメント)の形式を決めました。 CUE自体は以前から知っており、Goライブラリ(cuelang.org/go)でCUE定義をパース・デコードできることは把握していたため、CLIへの組み込みは自然な流れでした。

インターフェースと入出力が固まった状態でClaude Codeに実装を任せたところ、開発自体は数時間で完了しました。

導入してみて

lattice導入後、「背景」で挙げた課題に対して以下のような変化がありました。

導入効果

  • 宣言的な定義への移行: フロントエンドの表示制御ロジックがCUE定義に集約され、命令的なif文実装がなくなった
  • ドキュメントの自動生成: 「どのプラン×ロールで何が使えるか」をコードを読まなくても確認できるようになった
  • コード量の削減: 表示制御ロジックを手書きする箇所が減り、管理対象を絞れた

特に、条件が複雑なケースでもロジックをif文で追いかける必要がなくなり、非エンジニアを含めた仕様確認もしやすくなりました。

移行プロセス

  • 判定関数自体は1つのファイルに集約されていたため、そのファイル内の関数をlatticeの生成コードに差し替える形で移行できた
  • 検証では、既存の表示制御関数のテストをそのまま流用し、すべてパスすることでデグレがないことを確認した
  • 一方で、表示制御ロジックが画面ごとに分散している構成では、移行の手間は大きくなる可能性がある

テストと運用

  • アクセス制御関数の生成は開発時に手元で実行し、生成されたコードをリポジトリにコミットしている
  • lattice側に不具合が混じる可能性もあるため、生成コードをそのまま信頼するのではなく、アプリ側にアクセス制御関数の自動テストを入れるようにしている
  • CUE定義と生成コードの同期はまだCIに組み込めておらず、再生成忘れを防ぐ仕組みは今後の課題である

CUEの所感

  • 型と値の一体化による制約表現の柔軟さは、実運用でも効果があった
  • 「定義したプランの中からのみ参照可能」といった制約を定義時に検証できるため、設定ミスをコード生成前に防げた
  • ただし、latticeはアクセス制御ルールの管理を容易にするものであり、アクセス制御モデル自体の複雑さを解消するものではない

運用上の注意点と今後の課題

  • IDEやLSPサーバーのサポートには改善の余地がある。検証環境ではIDE補完が十分に効かない場面があり、CUEエコシステムの成熟による改善を期待している
  • 現時点ではプラン×ロールの静的な組み合わせで十分だったため、ユーザー属性に基づく動的なアクセス制御には対応していない。必要になった場合は別のアプローチを検討する予定
  • バックエンドへの導入は未着手であり、今後の対応範囲である
  • 導入からまだ1ヶ月であり定量的な効果の計測はこれからだが、アクセス制御ルールの宣言的な定義が可能になったことで、アクセス制御ロジックの管理はしやすくなった

おわりに

本記事では、CUEを利用したアクセス制御のコード生成について紹介しました。

設定ファイルからコードを生成する手法は、アクセス制御のように条件の組み合わせが複雑で正確性の求められる領域で特に有効です。 少なくとも今回のように、設定駆動でアクセス制御ルールを管理するユースケースでは、CUEは実用的な選択肢になると感じています。


enechainでは、事業拡大のために随時仲間を募集しています。興味がある方はぜひお声がけください!

tech.enechain.com

herp.careers