概要
Firestoreのセキュリティルールを実装し、@keio.jpドメイン制限とデータアクセス制御を確実にする。
前提条件
- Firestore にデータ投入済み
- Firebase Authentication 設定完了
- 基本的なAngularFire設定完了
タスク一覧
1. Security Rules ファイル作成
2. 認証・認可ルールの実装
3. コレクション別アクセス制御
4. データ検証ルール
5. Security Rules のテスト
Security Rules 実装
完全な firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ========================================
// Helper Functions
// ========================================
// 認証済みユーザーかチェック
function isAuthenticated() {
return request.auth \!= null;
}
// @keio.jpドメインのユーザーかチェック
function isKeioUser() {
return isAuthenticated() &&
request.auth.token.email.matches('.*@keio\.jp$');
}
// リソースの所有者かチェック
function isOwner(userId) {
return isKeioUser() && request.auth.uid == userId;
}
// 管理者かチェック(将来的な拡張用)
function isAdmin() {
return isKeioUser() &&
exists(/databases/$(database)/documents/admins/$(request.auth.uid));
}
// ========================================
// Data Validation Functions
// ========================================
// ユーザーデータの検証
function validateUserData() {
let data = request.resource.data;
let requiredFields = ['uid', 'email', 'createdAt'];
let allowedFields = ['uid', 'email', 'displayName', 'facultyType', 'bio', 'createdAt', 'updatedAt'];
return data.keys().hasAll(requiredFields) &&
data.keys().hasOnly(allowedFields) &&
data.uid is string &&
data.email is string &&
data.email.matches('.*@keio\.jp$') &&
data.createdAt is timestamp &&
(data.displayName == null || data.displayName is string) &&
(data.facultyType == null || data.facultyType is string) &&
(data.bio == null || (data.bio is string && data.bio.size() <= 500));
}
// 投稿データの検証
function validatePostData() {
let data = request.resource.data;
let requiredFields = ['title', 'content', 'postType', 'subjectId', 'userId', 'createdAt'];
let allowedFields = ['title', 'content', 'postType', 'subjectId', 'userId', 'parentPostId', 'likeCount', 'createdAt', 'updatedAt'];
return data.keys().hasAll(requiredFields) &&
data.keys().hasOnly(allowedFields) &&
data.title is string &&
data.title.size() > 0 && data.title.size() <= 200 &&
data.content is string &&
data.content.size() > 0 && data.content.size() <= 10000 &&
data.postType in ['question', 'answer', 'review'] &&
data.subjectId is string &&
data.userId is string &&
data.userId == request.auth.uid &&
data.createdAt is timestamp &&
(data.parentPostId == null || data.parentPostId is string) &&
(data.likeCount == null || (data.likeCount is number && data.likeCount >= 0));
}
// ========================================
// Collection Rules
// ========================================
// Users Collection
match /users/{userId} {
// 自分の情報のみ読み書き可能
allow read, write: if isOwner(userId);
// 新規ユーザー作成時の特別ルール
allow create: if isKeioUser() &&
request.auth.uid == userId &&
validateUserData();
}
// Subjects Collection (読み取り専用)
match /subjects/{subjectId} {
// @keio.jpユーザーのみ読み取り可能
allow read: if isKeioUser();
// 書き込みは管理者のみ(将来的に)
allow write: if isAdmin();
}
// Posts Collection
match /posts/{postId} {
// @keio.jpユーザーは全投稿を読み取り可能
allow read: if isKeioUser();
// 投稿作成は認証済み@keio.jpユーザーのみ
allow create: if isKeioUser() &&
request.auth.uid == request.resource.data.userId &&
validatePostData();
// 投稿の更新・削除は作成者のみ
allow update, delete: if isKeioUser() &&
request.auth.uid == resource.data.userId;
// Likes Subcollection
match /likes/{likeUserId} {
// いいねの読み取りは@keio.jpユーザー全員
allow read: if isKeioUser();
// いいねの作成・削除は自分のもののみ
allow create, delete: if isOwner(likeUserId);
}
}
// ========================================
// Admin Collection (将来的な拡張用)
// ========================================
match /admins/{adminId} {
allow read: if isOwner(adminId);
allow write: if false; // システム管理者のみ
}
}
}
Security Rules テスト
テストケース作成
// test/firestore-rules.test.js
const { assertFails, assertSucceeds, initializeTestEnvironment } = require('@firebase/rules-unit-testing');
describe('Firestore Security Rules', () => {
let testEnv;
beforeEach(async () => {
testEnv = await initializeTestEnvironment({
projectId: 'kcc-syllabus-test',
firestore: {
rules: fs.readFileSync('firestore.rules', 'utf8'),
},
});
});
afterEach(async () => {
await testEnv.cleanup();
});
describe('Authentication', () => {
test('@keio.jpユーザーはsubjectsを読み取れる', async () => {
const db = testEnv.authenticatedContext('test-user', {
email: 'test@keio.jp'
}).firestore();
await assertSucceeds(db.collection('subjects').doc('test').get());
});
test('非@keio.jpユーザーはsubjectsを読み取れない', async () => {
const db = testEnv.authenticatedContext('test-user', {
email: 'test@gmail.com'
}).firestore();
await assertFails(db.collection('subjects').doc('test').get());
});
});
describe('Users Collection', () => {
test('ユーザーは自分の情報のみアクセス可能', async () => {
const db = testEnv.authenticatedContext('user1', {
email: 'user1@keio.jp'
}).firestore();
await assertSucceeds(db.collection('users').doc('user1').get());
await assertFails(db.collection('users').doc('user2').get());
});
});
describe('Posts Collection', () => {
test('適切な投稿データの作成が成功する', async () => {
const db = testEnv.authenticatedContext('user1', {
email: 'user1@keio.jp'
}).firestore();
const validPost = {
title: 'テスト質問',
content: 'テスト内容',
postType: 'question',
subjectId: 'subject1',
userId: 'user1',
createdAt: new Date()
};
await assertSucceeds(db.collection('posts').add(validPost));
});
test('不正な投稿データは拒否される', async () => {
const db = testEnv.authenticatedContext('user1', {
email: 'user1@keio.jp'
}).firestore();
const invalidPost = {
title: '', // 空のタイトル
content: 'テスト内容',
postType: 'invalid', // 無効なpostType
userId: 'user1'
};
await assertFails(db.collection('posts').add(invalidPost));
});
});
});
インデックス設定
firestore.indexes.json
{
"indexes": [
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{"fieldPath": "subjectId", "order": "ASCENDING"},
{"fieldPath": "createdAt", "order": "DESCENDING"}
]
},
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{"fieldPath": "userId", "order": "ASCENDING"},
{"fieldPath": "createdAt", "order": "DESCENDING"}
]
},
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{"fieldPath": "parentPostId", "order": "ASCENDING"},
{"fieldPath": "postType", "order": "ASCENDING"},
{"fieldPath": "createdAt", "order": "ASCENDING"}
]
}
]
}
デプロイとモニタリング
Rules デプロイ
# Security Rules のデプロイ
firebase deploy --only firestore:rules
# インデックスのデプロイ
firebase deploy --only firestore:indexes
Acceptance Criteria
セキュリティテスト項目
参照
次のステップ
Issue #8: 認証機能とユーザー管理の完全実装
概要
Firestoreのセキュリティルールを実装し、@keio.jpドメイン制限とデータアクセス制御を確実にする。
前提条件
タスク一覧
1. Security Rules ファイル作成
firestore.rulesファイルの作成2. 認証・認可ルールの実装
3. コレクション別アクセス制御
4. データ検証ルール
5. Security Rules のテスト
Security Rules 実装
完全な firestore.rules
Security Rules テスト
テストケース作成
インデックス設定
firestore.indexes.json
{ "indexes": [ { "collectionGroup": "posts", "queryScope": "COLLECTION", "fields": [ {"fieldPath": "subjectId", "order": "ASCENDING"}, {"fieldPath": "createdAt", "order": "DESCENDING"} ] }, { "collectionGroup": "posts", "queryScope": "COLLECTION", "fields": [ {"fieldPath": "userId", "order": "ASCENDING"}, {"fieldPath": "createdAt", "order": "DESCENDING"} ] }, { "collectionGroup": "posts", "queryScope": "COLLECTION", "fields": [ {"fieldPath": "parentPostId", "order": "ASCENDING"}, {"fieldPath": "postType", "order": "ASCENDING"}, {"fieldPath": "createdAt", "order": "ASCENDING"} ] } ] }デプロイとモニタリング
Rules デプロイ
Acceptance Criteria
セキュリティテスト項目
参照
次のステップ
Issue #8: 認証機能とユーザー管理の完全実装