2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TSの鬼 第13回:型で縛る権限管理モデル—Role/Resource/Action を静的保証する

Posted at

はじめに

前回

Web アプリケーションが複雑化するにつれ アクセス制御 はバグの温床になりやすい。本稿では TypeScript の型システムと条件付き型を活用し、実装段階で不正アクセスをコンパイルエラーにする 設計手法を解説する。


1. 権限モデルを分解する

概念 備考
Role "admin", "editor", "viewer" ユーザー属性
Resource "article", "comment" 対象リソース
Action "create", "read", "update", "delete" 操作

Role × Resource × Action の直積がアクセス制御表となる。


2. 権限マトリクスを型で宣言

const ACL = {
  admin: {
    article: ["create", "read", "update", "delete"],
    comment: ["create", "read", "update", "delete"],
  },
  editor: {
    article: ["create", "read", "update"],
    comment: ["read", "update"],
  },
  viewer: {
    article: ["read"],
    comment: ["read"],
  },
} as const;
  • as const でリテラル型を保持し、配列要素が Union 型に展開される。
type Role = keyof typeof ACL;                // "admin" | "editor" | "viewer"
type Resource<R extends Role> = keyof typeof ACL[R];

3. 型安全な canAccess 関数

type CanAccess<
  R extends Role,
  Res extends Resource<R>,
  Act extends (typeof ACL)[R][Res][number],
> = true;

// 使用例
const ok: CanAccess<"editor", "article", "update"> = true; // ✅
// const ng: CanAccess<"viewer", "article", "update"> = true; // ❌ 型エラー
  • ジェネリクスと indexed access type で 許可された組み合わせのみ 型が解決する。

実装版

function canAccess<R extends Role, Res extends Resource<R>>(
  role: R,
  resource: Res,
  action: (typeof ACL)[R][Res][number]
): boolean {
  return ACL[role][resource].includes(action as any);
}

コンパイル時は型保証、ランタイムは配列検索で軽量。


4. API ルートガードに適用

// Next.js Middleware 例
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  const role = req.headers.get("x-role") as Role;
  const url = new URL(req.url);
  const [_, res, id] = url.pathname.split("/");
  const action = req.method === "GET" ? "read" : "update";

  if (!canAccess(role, res as any, action as any)) {
    return new Response("Forbidden", { status: 403 });
  }
}

IDE 補完でリソースとアクションが選択肢として出るため、ミススペルや未定義操作を事前排除 できる。


5. 動的 Role 追加を考慮した拡張

type Merge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof B
    ? B[K]
    : K extends keyof A
    ? A[K]
    : never;
};

type ExtendedACL = Merge<typeof ACL, {
  guest: {
    article: ["read"];
    comment: [];
  };
}>;
  • 型ユーティリティでマトリクスをマージし、既存ロジックを崩さず拡張。

6. 落とし穴と対策

落とし穴 原因 対策
配列要素がリテラル型化されない as const を忘れる すべての ACL オブジェクトに as const を付与
ACL が肥大化し可読性低下 大規模 Role 数 Domain ごとに ACL を分割し Merge で合成
ランタイム追加 Role 静的型に含まれず any DB から ACL を読み込み、as const 生成を自動スクリプト化

まとめ

  • Role × Resource × Action をリテラルで定義し、Union 型へ展開。
  • indexed access とジェネリクスで 許可組み合わせのみ 関数を受け付ける。
  • ランタイムでも軽量チェックを行い、静的・動的の二段構えで安全を確保する。

次回は キャッシュ戦略と型保証 をテーマに、React Query と Suspense を用いたデータ整合性モデルを深掘りする。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?