アプリケーションを作っていると、権限制御が必要になってくることがあります。
幸いなことに、権限制御の詳しい内容を理解しなくても、各プログラミング言語ごとに便利なライブラリがあります。このブログでは過去にいくつかのライブラリをさわってきました。
- Pundit + Rolify を使って、Rails製APIアプリでロールによる認可制御を行ってみた - メモ的な思考的な
- django-rulesを使って、オブジェクトレベルの認可判定をViewとテンプレートでそれぞれ実装してみた - メモ的な思考的な
一方で、権限制御まわりはライブラリにお任せしてきたため、以下のような権限制御の記事を読んでも「ACL、RBAC、ABACなどの概念は分かるけど、権限の仕組みの違いは雰囲気でしか理解してないかも...」と思うこともよくありました。
アプリケーションにおける権限設計の課題 - kenfdev’s blog
ただ、それらの権限制御の概念は、OAuthやOpenID Connectとは異なりRFCなどで標準化された規格的なものがない認識です*1。そのため、個人で何か作りながら学ぼうとしても、「世間一般で言われている権限制御の概念はこれでいいのかな...」となっていました。
そんな中、Claude Codeを始めとする生成AIが最近出てきたことで、「世間一般で言われている権限制御の概念は、生成AIとともに学べばいいのでは...?」となりました。
そこで、まずはUnixのパーミッションはどんな仕組みで実現できるのか実装してみたことから、メモを残します。
目次
環境
- mac
- Claude Code + Opus 4.0/4.1
- 学んでいる最中にOpusのバージョンが上がったため、両方使うことになりました
- TypeScript 5.7.3
- Bun 1.2.19
- Biome 2.1.2
学び方の方針
最初は「Webアプリケーションを作成して、ユーザーごとの権限管理ができているかを確認する」みたいなことを考えていました。
ただ、それだと権限管理以外のことも気にしないといけないため、Claudeに「権限管理を学びたい背景と、どんな内容を学びたいか」について相談し、
- Webアプリケーションではなく、権限管理のロジックだけを実装したクラスを用意する
- ユーザーごとの権限管理は、テストコードで動作を確認する
方針でいくことにしました。
学び方の細かいところはこんな感じです。
- 題材は、ファイル管理システム
- リソースに対する権限制御をクラスとして定義
- 1ファイル1インスタンスで、権限有無を確認
- プログラミング言語はTypeScript
- 使ってみたかっただけ
- 実行環境はBun
- TypeScriptの実行が容易なのと、テストランナーがBunに組み込まれているため
- メインのロジックは自分で実装するが、それ以外の部分(型・クラス・メソッドなどの枠やテストコードなど)はClaude Codeに任せる
- ただし、Claude Codeが出したコードはレビューする
今回実装するUnixのファイルパーミッションについて
Unixのファイルパーミッションの仕様について、Wikipediaには以下のように記載されています。
ファイルごとに定義された、読み出し・書き込みなどのアクセスに対する許可情報。通常は、ファイルシステム内のファイルごとに、特定のユーザーやグループに対してアクセス権を設定する。
ただ、今回は学習用途なので、Wikipediaの内容からもう少し簡単な仕様としました。
- ファイルの実行権限は考慮しない
rootユーザーなどの特殊なケースは考慮しない
実装
権限とリソースの表現
PermissionBits 型にて、読み込み権限・書き込み権限の有無を表現します。
export type PermissionBits = { read: boolean write: boolean }
次に、 Mode 型にて、ユーザー(owner)、グループ(group)、その他のユーザー(others)ごとに、読み込み・書き込み権限があるかを表現します。
export type Mode = { owner: PermissionBits group: PermissionBits others: PermissionBits }
続いて、ファイルのようなリソースを UnixResource 型として表現します。 name はリソースの名前(foo.txt)、ownerやgroupはそれぞれの名前を設定する想定です。また、このリソースに対する権限制御は permissions に定義します。
なお、今回は学習用途の簡易的な実装とするためディレクトリ・ファイルの区別は行わず、それらをまるっとリソースとして表現します。
export type UnixResource = { name: string owner: string group: string permissions: Mode }
権限判定処理
リソースを保有するクラス UnixPermission を定義します。これはリソースごとに1インスタンスを作成する想定です。
export class UnixPermission { private resource: UnixResource constructor(resource: UnixResource) { this.resource = resource } // 略 }
このクラスには、権限判定を行うメソッド hasPermission を定義します。
このメソッドではユーザーとユーザーが所属するグループを受け取ることで、権限の判定を行います。Unixのパーミッションには「特定のユーザー・グループを明示的に拒否する」という概念がないため、ユーザー名やグループを元に権限有無をチェックするだけな処理となりました。
また、「Unixパーミッションは所有者 > グループ > その他の順で判定され、最初にマッチしたカテゴリで権限の有無を判断する」も実装しています。これにより、ある所有者が「所有者とグループでマッチする」状況で
- 所有者では、書き込み不可
- グループでは、書き込み可
という権限設定の場合、hasPermissionは「書き込み不可(=最初にマッチした所有者)」と判定します。
export class UnixPermission { hasPermission(userName: string, userGroupNames: string[], action: 'read' | 'write'): boolean { if (userName === this.resource.owner) { return this.resource.permissions.owner[action] } if (userGroupNames.includes(this.resource.group)) { return this.resource.permissions.group[action] } return this.resource.permissions.others[action] } }
動作確認
今回はテストコードで動作を確認します。
以前の記事で見たように、Bunではパラメタライズドテストができます。
- test.each and describe.each | Writing tests – Test runner | Bun Docs
- Bun 1.2.19から、test.eachやit.eachを使ったパラメタライズドテストにて、ラベルの中の変数展開が行われるようになった - メモ的な思考的な
今回のテストでは「ユーザーとグループとactionの組み合わせで権限の有無が確認できる」ことから、パラメタライズドテストで検証することにしました。こんな感じです。
describe('UnixPermission', () => { describe('hasPermission', () => { describe('読み込み権限について', () => { describe('ownerのみ権限がある', () => { const resource: UnixResource = { name: 'test.txt', owner: 'my_user', group: 'my_group', permissions: { owner: { read: true, write: false }, group: { read: false, write: false }, others: { read: false, write: false } } } const permission = new UnixPermission(resource) it.each([ { title: 'ユーザーがowner', userName: 'my_user', groupNames: ['another_group'], expected: true }, { title: 'ユーザーが単一groupに所属', userName: 'another_user', groupNames: ['my_group'], expected: false }, { title: 'ユーザーが複数groupに所属', userName: 'another_user', groupNames: ['my_group', 'another_group'], expected: false }, { title: 'ユーザーがowner、かつ、groupに所属', userName: 'my_user', groupNames: ['my_group'], expected: true }, { title: 'ユーザーが何も所属していない', userName: 'another_user', groupNames: ['another_group'], expected: false } ])('$title 時の結果が $expected であること', ({ userName, groupNames, expected }) => { expect(permission.hasPermission(userName, groupNames, 'read')).toBe(expected) }) }) // 略
テストコードの全体は後述のソースコードを確認してください。
テストを実行したところすべてのテストがパスしたことから、Unixのファイルパーミッションが実装できました。
ソースコード
GitHubに上げました。
https://github.com/thinkAmi-sandbox/authorization_practice_in_memory
今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/authorization_practice_in_memory/pull/1/files
なお、Claude Codeに任せた部分と自分で書いた部分は、それぞれ別のコミットにしています。また、Claude Codeに任せたコミットには、Claude Codeに対するプロンプトも記載してあります。
*1:認識が誤っていたらツッコミをお願いします