リソースレベル権限システム移行ガイド¶
概要¶
このガイドは、グローバルロールベースの認証システムからリソースレベル権限システムへの移行手順を説明します。
変更の概要¶
Before(現行システム)¶
- ユーザーはシステム全体で1つのロール(User, LimitedAdmin, ReadOnlyAdmin, FullAccessAdmin)を持つ
- 管理者権限は全プロジェクト・全企画に対して有効
- 企画のマネージャーは「最初のメンバー」という暗黙的なルール
After(新システム)¶
- ユーザーはリソース(Project, CircleProject)ごとに異なる権限を持てる
- 細かい粒度の権限(READ, WRITE, DELETE, MANAGE_MEMBERS等)をリソース単位で付与
- ロールテンプレートにより、よく使う権限セットを簡単に適用可能
- グローバルロールは下位互換性のために残すが、段階的に廃止
データベーススキーマの変更¶
新規テーブル¶
1. ResourcePermission¶
リソースレベルの権限を管理するテーブル。
model ResourcePermission {
id String @id @default(dbgenerated("(UUID())"))
user User @relation(name: "user_resource_permissions", fields: [userId], references: [id], onDelete: Cascade)
userId String
resourceType ResourceType // PROJECT or CIRCLE_PROJECT
resourceId String // Project.prefix or CircleProject.id
permissions String @db.Text // JSON配列形式で権限を保存
grantedBy String? // 権限を付与したユーザーID
grantedAt DateTime @default(now())
expiresAt DateTime? // 権限の有効期限(オプション)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
@@unique([userId, resourceType, resourceId])
@@index([resourceType, resourceId])
@@index([userId])
}
2. ResourceRoleTemplate¶
プリセット権限セットを定義するテーブル。
model ResourceRoleTemplate {
id String @id @default(dbgenerated("(UUID())"))
name String // ロール名 (例: "Manager", "Editor", "Viewer")
resourceType ResourceType // 対象リソースタイプ
permissions String @db.Text // JSON配列形式で権限を保存
description String? @db.Text // ロールの説明
isSystem Boolean @default(false) // システム標準ロールかどうか
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
@@unique([name, resourceType])
}
新規Enum¶
enum ResourceType {
PROJECT // イベント全体
CIRCLE_PROJECT // 個別企画
}
enum Permission {
READ // 読み取り
WRITE // 書き込み・更新
DELETE // 削除
MANAGE_MEMBERS // メンバー管理
MANAGE_PERMISSIONS // 権限管理
APPROVE // 承認操作
CHECKIN // チェックイン操作
ALLOCATE_RESOURCES // リソース割り当て
VIEW_PRIVATE // 非公開情報の閲覧
}
マイグレーション手順¶
Phase 1: スキーマ更新とデータ準備¶
- Prismaスキーマ更新
- ロールテンプレートのシード
web/prisma/seed-resource-roles.ts を作成して実行:
Phase 2: 既存データの移行¶
- 既存権限の移行スクリプト実行
グローバルロールを持つユーザーに対して、適切なリソースレベル権限を付与:
// 移行スクリプト例
async function migrateExistingPermissions() {
// FullAccessAdminユーザーは全プロジェクトにProjectAdmin権限を付与
const admins = await prisma.user.findMany({
where: { role: 'FullAccessAdmin' }
});
const projects = await prisma.project.findMany();
for (const admin of admins) {
for (const project of projects) {
await prisma.resourcePermission.create({
data: {
userId: admin.id,
resourceType: 'PROJECT',
resourceId: project.prefix,
permissions: JSON.stringify([
'READ', 'WRITE', 'DELETE', 'MANAGE_MEMBERS',
'MANAGE_PERMISSIONS', 'APPROVE', 'ALLOCATE_RESOURCES', 'VIEW_PRIVATE'
]),
grantedBy: admin.id,
}
});
}
}
// CircleProjectの既存メンバーに権限を付与
// 最初のメンバー(マネージャー)にManager権限
// その他のメンバーにMember権限
const circleProjects = await prisma.circleProject.findMany({
include: {
members: {
orderBy: { id: 'asc' }
}
}
});
for (const cp of circleProjects) {
for (let i = 0; i < cp.members.length; i++) {
const isManager = i === 0;
const permissions = isManager
? ['READ', 'WRITE', 'DELETE', 'MANAGE_MEMBERS', 'MANAGE_PERMISSIONS', 'CHECKIN', 'VIEW_PRIVATE']
: ['READ', 'CHECKIN'];
await prisma.resourcePermission.create({
data: {
userId: cp.members[i].userId,
resourceType: 'CIRCLE_PROJECT',
resourceId: cp.id,
permissions: JSON.stringify(permissions),
}
});
}
}
}
Phase 3: コード更新¶
- 認証ヘルパー関数の利用
新しいヘルパー関数を利用してエンドポイントを更新:
// Before
import { requireAdmin } from '@/lib/api/auth';
app.put('/api/project', requireAdmin(), async (c) => {
// ...
});
// After
import { requireResourcePermission } from '@/lib/api/auth';
app.put('/api/project/:prefix',
requireResourcePermission('PROJECT', 'prefix', ['WRITE']),
async (c) => {
// ...
}
);
- エンドポイント実装例
// 企画の更新エンドポイント
app.put('/api/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(updated);
}
);
// メンバー管理エンドポイント
app.post('/api/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 });
}
);
Phase 4: 権限管理エンドポイントの実装¶
- リソース権限管理API
以下のエンドポイントを実装:
GET /api/resource-permissions- 権限取得POST /api/resource-permissions- 権限付与PUT /api/resource-permissions/:id- 権限更新DELETE /api/resource-permissions/:id- 権限削除GET /api/resource-role-templates- ロールテンプレート一覧
詳細は API_SPECIFICATION.md を参照。
Phase 5: フロントエンド更新¶
-
UIコンポーネント作成
-
リソースごとの権限管理画面
- ユーザーに権限を付与するフォーム
-
ロールテンプレート選択UI
-
権限チェックのフック
// useResourcePermission.ts
export function useResourcePermission(
resourceType: ResourceType,
resourceId: string,
requiredPermissions: Permission[]
) {
const [hasPermission, setHasPermission] = useState(false);
useEffect(() => {
fetch(`/api/resource-permissions/check?resourceType=${resourceType}&resourceId=${resourceId}&permissions=${requiredPermissions.join(',')}`)
.then(r => r.json())
.then(data => setHasPermission(data.hasPermission));
}, [resourceType, resourceId, requiredPermissions]);
return hasPermission;
}
Phase 6: 段階的なグローバルロール廃止¶
-
FullAccessAdminの保持
-
FullAccessAdminは全リソースに対して全権限を持つ(後方互換性)
-
新規ユーザーにはリソースレベル権限のみを付与
-
古いロールの段階的廃止
-
LimitedAdmin, ReadOnlyAdminを新規付与しない
- 既存ユーザーは適切なリソースレベル権限に移行
- 移行完了後、User.role フィールドをオプショナルまたは削除
標準ロールテンプレート¶
Project用¶
| ロール名 | 権限 |
|---|---|
| ProjectAdmin | READ, WRITE, DELETE, MANAGE_MEMBERS, MANAGE_PERMISSIONS, APPROVE, ALLOCATE_RESOURCES, VIEW_PRIVATE |
| ProjectManager | READ, WRITE, APPROVE, ALLOCATE_RESOURCES, VIEW_PRIVATE |
| ProjectEditor | READ, WRITE, VIEW_PRIVATE |
| ProjectViewer | READ |
CircleProject用¶
| ロール名 | 権限 |
|---|---|
| Manager | READ, WRITE, DELETE, MANAGE_MEMBERS, MANAGE_PERMISSIONS, CHECKIN, VIEW_PRIVATE |
| Editor | READ, WRITE, CHECKIN, VIEW_PRIVATE |
| Member | READ, CHECKIN |
| Viewer | READ |
使用例¶
例1: 特定のプロジェクトに管理者を追加¶
// ProjectManagerロールを付与
await prisma.resourcePermission.create({
data: {
userId: 'user-uuid',
resourceType: 'PROJECT',
resourceId: 'chibafes2024',
permissions: JSON.stringify(['READ', 'WRITE', 'APPROVE', 'ALLOCATE_RESOURCES', 'VIEW_PRIVATE']),
grantedBy: 'admin-uuid',
}
});
例2: 企画のマネージャーを変更¶
// 現在のマネージャーの権限を更新
await prisma.resourcePermission.update({
where: {
userId_resourceType_resourceId: {
userId: 'old-manager-uuid',
resourceType: 'CIRCLE_PROJECT',
resourceId: 'circle-project-uuid',
}
},
data: {
permissions: JSON.stringify(['READ', 'CHECKIN']), // Memberに降格
}
});
// 新しいマネージャーに権限付与
await prisma.resourcePermission.create({
data: {
userId: 'new-manager-uuid',
resourceType: 'CIRCLE_PROJECT',
resourceId: 'circle-project-uuid',
permissions: JSON.stringify(['READ', 'WRITE', 'DELETE', 'MANAGE_MEMBERS', 'MANAGE_PERMISSIONS', 'CHECKIN', 'VIEW_PRIVATE']),
grantedBy: 'admin-uuid',
}
});
例3: 期限付き権限の付与¶
// 1ヶ月間のみ編集権限を付与
await prisma.resourcePermission.create({
data: {
userId: 'temp-editor-uuid',
resourceType: 'PROJECT',
resourceId: 'chibafes2024',
permissions: JSON.stringify(['READ', 'WRITE']),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30日後
grantedBy: 'admin-uuid',
}
});
トラブルシューティング¶
問題: 既存のエンドポイントが動作しない¶
原因: グローバルロールベースの認証ミドルウェアを使用している
解決策: 新しい requireResourcePermission ミドルウェアに置き換える
問題: FullAccessAdminが特定のリソースにアクセスできない¶
原因: 新しい権限チェックロジックの問題
解決策: hasResourcePermission 関数でFullAccessAdminの特別扱いを確認
問題: パフォーマンスの低下¶
原因: 権限チェックのたびにDBクエリが発生
解決策: - Redis等を使った権限キャッシュの実装 - JWTに権限情報を含める - バッチ処理での権限チェック
ベストプラクティス¶
- 最小権限の原則: 必要最小限の権限のみを付与
- 期限の設定: 一時的な権限には必ず有効期限を設定
- 監査ログ: 権限の付与・変更・削除をログに記録
- 定期的なレビュー: 不要な権限が残っていないか定期的に確認
- テンプレートの活用: カスタム権限セットよりもロールテンプレートを優先
参考資料¶
- API_SPECIFICATION.md - API仕様書
- web/prisma/schema.prisma - データベーススキーマ
- web/lib/api/auth.ts - 認証ヘルパー関数