コンテンツにスキップ

リソースレベル権限システム 使用例

このドキュメントでは、リソースレベル権限システムの具体的な使用例を紹介します。

目次

  1. 基本的な権限の付与
  2. ロールテンプレートの使用
  3. エンドポイントでの権限チェック
  4. プログラムからの権限チェック
  5. フロントエンドでの使用

基本的な権限の付与

例1: ユーザーにプロジェクトの閲覧権限を付与

curl -X POST http://localhost:3000/api/resource-permissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "userId": "user-uuid-123",
    "resourceType": "PROJECT",
    "resourceId": "chibafes2024",
    "permissions": ["READ"]
  }'

例2: ユーザーに企画の編集権限を付与(期限付き)

curl -X POST http://localhost:3000/api/resource-permissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "userId": "user-uuid-456",
    "resourceType": "CIRCLE_PROJECT",
    "resourceId": "circle-project-uuid-789",
    "permissions": ["READ", "WRITE", "CHECKIN"],
    "expiresAt": "2025-12-31T23:59:59Z"
  }'

例3: 複数の権限を一度に付与

curl -X POST http://localhost:3000/api/resource-permissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "userId": "user-uuid-789",
    "resourceType": "PROJECT",
    "resourceId": "chibafes2024",
    "permissions": ["READ", "WRITE", "APPROVE", "ALLOCATE_RESOURCES"]
  }'

ロールテンプレートの使用

例4: ロールテンプレート一覧の取得

# 全てのロールテンプレートを取得
curl -X GET http://localhost:3000/api/resource-role-templates \
  -H "Authorization: Bearer YOUR_TOKEN"

# PROJECT用のロールテンプレートのみ取得
curl -X GET "http://localhost:3000/api/resource-role-templates?resourceType=PROJECT" \
  -H "Authorization: Bearer YOUR_TOKEN"

レスポンス例:

{
  "templates": [
    {
      "id": "template-uuid-1",
      "name": "ProjectManager",
      "resourceType": "PROJECT",
      "permissions": ["READ", "WRITE", "APPROVE", "ALLOCATE_RESOURCES", "VIEW_PRIVATE"],
      "description": "プロジェクト管理者用ロール",
      "isSystem": true
    },
    {
      "id": "template-uuid-2",
      "name": "ProjectEditor",
      "resourceType": "PROJECT",
      "permissions": ["READ", "WRITE", "VIEW_PRIVATE"],
      "description": "プロジェクト編集者用ロール",
      "isSystem": true
    }
  ]
}

例5: ロールテンプレートを使用して権限を付与

curl -X POST http://localhost:3000/api/resource-permissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "userId": "user-uuid-123",
    "resourceType": "PROJECT",
    "resourceId": "chibafes2024",
    "roleTemplate": "ProjectManager"
  }'

例6: カスタムロールテンプレートの作成

curl -X POST http://localhost:3000/api/resource-role-templates \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "name": "EventCoordinator",
    "resourceType": "PROJECT",
    "permissions": ["READ", "WRITE", "VIEW_PRIVATE"],
    "description": "イベントコーディネーター用のカスタムロール"
  }'

エンドポイントでの権限チェック

例7: 新しいエンドポイントでリソース権限を使用

// endpoints/my-protected-endpoint.ts
import { Hono } from 'hono';
import { requireResourcePermission } from '../lib/api/auth';

const app = new Hono();

// 企画の更新エンドポイント(WRITE権限が必要)
app.put('/circle-project/:id',
  requireResourcePermission('CIRCLE_PROJECT', 'id', ['WRITE']),
  async (c) => {
    const id = c.req.param('id');
    const body = await c.req.json();

    // 権限チェック済みなので直接更新可能
    const updated = await prisma.circleProject.update({
      where: { id },
      data: body,
    });

    return c.json({ success: true, data: updated });
  }
);

// メンバー管理エンドポイント(MANAGE_MEMBERS権限が必要)
app.post('/circle-project/:id/members',
  requireResourcePermission('CIRCLE_PROJECT', 'id', ['MANAGE_MEMBERS']),
  async (c) => {
    const id = c.req.param('id');
    const { userId } = await c.req.json();

    await prisma.projectMember.create({
      data: { circleProjectId: id, userId },
    });

    return c.json({ success: true });
  }
);

// 承認エンドポイント(APPROVE権限が必要)
app.post('/circle-project/:id/approve',
  requireResourcePermission('CIRCLE_PROJECT', 'id', ['APPROVE']),
  async (c) => {
    const id = c.req.param('id');

    const updated = await prisma.circleProject.update({
      where: { id },
      data: { status: 'APPROVED' },
    });

    return c.json({ success: true, data: updated });
  }
);

export default app;

プログラムからの権限チェック

例8: 関数内で権限をチェック

import { hasResourcePermission, getUserResourcePermissions } from '../lib/api/auth';

// 特定の権限を持っているかチェック
async function canUserEditProject(userId: string, projectId: string): Promise<boolean> {
  return await hasResourcePermission(
    userId,
    'CIRCLE_PROJECT',
    projectId,
    ['WRITE']
  );
}

// ユーザーが持つ全ての権限を取得
async function getUserProjectPermissions(userId: string, projectId: string) {
  const permissions = await getUserResourcePermissions(
    userId,
    'CIRCLE_PROJECT',
    projectId
  );

  console.log('User permissions:', permissions);
  // 出力例: ['READ', 'WRITE', 'CHECKIN']

  return permissions;
}

// 複数の権限をチェック
async function canUserManageProject(userId: string, projectId: string): Promise<boolean> {
  return await hasResourcePermission(
    userId,
    'CIRCLE_PROJECT',
    projectId,
    ['WRITE', 'MANAGE_MEMBERS', 'MANAGE_PERMISSIONS']
  );
}

例9: 条件分岐での使用

async function handleProjectUpdate(userId: string, projectId: string, updates: any) {
  // 権限に応じて編集可能なフィールドを制限
  const permissions = await getUserResourcePermissions(userId, 'CIRCLE_PROJECT', projectId);

  const allowedUpdates: any = {};

  // READ権限は誰でも持っているので除外
  if (permissions.includes('WRITE')) {
    // 基本的な編集
    allowedUpdates.name = updates.name;
    allowedUpdates.location = updates.location;
    allowedUpdates.description = updates.description;
  }

  if (permissions.includes('VIEW_PRIVATE')) {
    // 非公開情報の編集
    allowedUpdates.memo = updates.memo;
  }

  if (permissions.includes('APPROVE')) {
    // ステータスの変更
    allowedUpdates.status = updates.status;
  }

  if (permissions.includes('DELETE')) {
    // アーカイブ
    allowedUpdates.isArchived = updates.isArchived;
  }

  return await prisma.circleProject.update({
    where: { id: projectId },
    data: allowedUpdates,
  });
}

フロントエンドでの使用

例10: React フックで権限チェック

// hooks/useResourcePermission.ts
import { useState, useEffect } from 'react';
import type { ResourceType, Permission } from '@/lib/api/auth';

export function useResourcePermission(
  resourceType: ResourceType,
  resourceId: string,
  requiredPermissions: Permission[]
) {
  const [hasPermission, setHasPermission] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function checkPermission() {
      try {
        const response = await fetch(
          `/api/resource-permissions/check?` +
          `resourceType=${resourceType}&` +
          `resourceId=${resourceId}&` +
          `permissions=${requiredPermissions.join(',')}`
        );
        const data = await response.json();
        setHasPermission(data.hasPermission);
      } catch (error) {
        console.error('Permission check failed:', error);
        setHasPermission(false);
      } finally {
        setLoading(false);
      }
    }

    checkPermission();
  }, [resourceType, resourceId, requiredPermissions]);

  return { hasPermission, loading };
}

例11: 権限に応じたUIの表示/非表示

// components/CircleProjectActions.tsx
import { useResourcePermission } from '@/hooks/useResourcePermission';

function CircleProjectActions({ projectId }: { projectId: string }) {
  const { hasPermission: canEdit } = useResourcePermission(
    'CIRCLE_PROJECT',
    projectId,
    ['WRITE']
  );

  const { hasPermission: canManageMembers } = useResourcePermission(
    'CIRCLE_PROJECT',
    projectId,
    ['MANAGE_MEMBERS']
  );

  const { hasPermission: canApprove } = useResourcePermission(
    'CIRCLE_PROJECT',
    projectId,
    ['APPROVE']
  );

  return (
    <div className="action-buttons">
      {canEdit && (
        <button onClick={handleEdit}>編集</button>
      )}

      {canManageMembers && (
        <button onClick={handleManageMembers}>メンバー管理</button>
      )}

      {canApprove && (
        <button onClick={handleApprove}>承認</button>
      )}
    </div>
  );
}

例12: ユーザーの権限一覧を表示

// components/UserPermissions.tsx
import { useState, useEffect } from 'react';

function UserPermissions({ userId, resourceType, resourceId }: Props) {
  const [permissions, setPermissions] = useState([]);

  useEffect(() => {
    async function fetchPermissions() {
      const response = await fetch(
        `/api/resource-permissions?` +
        `userId=${userId}&` +
        `resourceType=${resourceType}&` +
        `resourceId=${resourceId}`
      );
      const data = await response.json();
      setPermissions(data.permissions);
    }

    fetchPermissions();
  }, [userId, resourceType, resourceId]);

  return (
    <div className="permissions-list">
      <h3>ユーザー権限</h3>
      <ul>
        {permissions.map((perm) => (
          <li key={perm.id}>
            <strong>{perm.resourceType}</strong> - {perm.resourceId}
            <ul>
              {perm.permissions.map((p) => (
                <li key={p}>{p}</li>
              ))}
            </ul>
            {perm.expiresAt && (
              <p>有効期限: {new Date(perm.expiresAt).toLocaleDateString()}</p>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}

例13: 権限付与フォーム

// components/GrantPermissionForm.tsx
import { useState } from 'react';

function GrantPermissionForm({ resourceType, resourceId }: Props) {
  const [selectedUser, setSelectedUser] = useState('');
  const [selectedTemplate, setSelectedTemplate] = useState('');
  const [templates, setTemplates] = useState([]);

  useEffect(() => {
    // ロールテンプレートを取得
    async function fetchTemplates() {
      const response = await fetch(
        `/api/resource-role-templates?resourceType=${resourceType}`
      );
      const data = await response.json();
      setTemplates(data.templates);
    }
    fetchTemplates();
  }, [resourceType]);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();

    const response = await fetch('/api/resource-permissions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId: selectedUser,
        resourceType,
        resourceId,
        roleTemplate: selectedTemplate,
      }),
    });

    if (response.ok) {
      alert('権限を付与しました');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>ユーザー</label>
        <select value={selectedUser} onChange={(e) => setSelectedUser(e.target.value)}>
          <option value="">選択してください</option>
          {/* ユーザー一覧 */}
        </select>
      </div>

      <div>
        <label>ロール</label>
        <select value={selectedTemplate} onChange={(e) => setSelectedTemplate(e.target.value)}>
          <option value="">選択してください</option>
          {templates.map((t) => (
            <option key={t.id} value={t.name}>
              {t.name} - {t.description}
            </option>
          ))}
        </select>
      </div>

      <button type="submit">権限を付与</button>
    </form>
  );
}

高度な使用例

例14: バッチ処理で複数ユーザーに権限を付与

async function grantPermissionsToMultipleUsers(
  userIds: string[],
  resourceType: ResourceType,
  resourceId: string,
  roleTemplate: string
) {
  const results = await Promise.allSettled(
    userIds.map((userId) =>
      fetch('/api/resource-permissions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          userId,
          resourceType,
          resourceId,
          roleTemplate,
        }),
      })
    )
  );

  const succeeded = results.filter((r) => r.status === 'fulfilled').length;
  const failed = results.filter((r) => r.status === 'rejected').length;

  console.log(`成功: ${succeeded}件, 失敗: ${failed}件`);
  return { succeeded, failed };
}

例15: 権限の移行スクリプト

// 既存のProjectMemberから権限を移行
async function migrateProjectMembersToResourcePermissions() {
  const circleProjects = await prisma.circleProject.findMany({
    include: {
      members: {
        orderBy: { id: 'asc' },
      },
    },
  });

  for (const project of circleProjects) {
    for (let i = 0; i < project.members.length; i++) {
      const isManager = i === 0; // 最初のメンバーがマネージャー
      const roleTemplate = isManager ? 'Manager' : 'Member';

      await fetch('/api/resource-permissions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          userId: project.members[i].userId,
          resourceType: 'CIRCLE_PROJECT',
          resourceId: project.id,
          roleTemplate,
        }),
      });
    }
  }

  console.log('Migration completed');
}

デバッグとテスト

例16: 権限チェックのデバッグ

# 特定のユーザーの権限を確認
curl -X GET "http://localhost:3000/api/resource-permissions/check?\
resourceType=CIRCLE_PROJECT&\
resourceId=circle-project-uuid&\
permissions=READ,WRITE,APPROVE" \
  -H "Authorization: Bearer USER_TOKEN"

レスポンス例:

{
  "hasPermission": false,
  "userPermissions": ["READ", "WRITE"],
  "requiredPermissions": ["READ", "WRITE", "APPROVE"],
  "missingPermissions": ["APPROVE"]
}

例17: 期限切れ権限のクリーンアップ

async function cleanupExpiredPermissions() {
  const expired = await prisma.resourcePermission.deleteMany({
    where: {
      expiresAt: {
        lt: new Date(),
      },
    },
  });

  console.log(`${expired.count}件の期限切れ権限を削除しました`);
}

まとめ

リソースレベル権限システムを使用することで、きめ細かいアクセス制御が可能になります。

主なポイント:

  1. 柔軟な権限管理: リソースごとに異なる権限を付与できる
  2. ロールテンプレート: よく使う権限セットを簡単に適用
  3. 期限付き権限: 一時的なアクセス権限を設定可能
  4. 後方互換性: FullAccessAdminは引き続き全権限を保持

詳細は以下のドキュメントを参照してください: - API_SPECIFICATION.md - API仕様 - MIGRATION_GUIDE.md - 移行ガイド