cropintel / firestore.rules
Jaithra Polavarapu
feat: add Firebase auth/farms backend alongside existing frontend
fb030a8
Raw
History Blame Contribute Delete
17.9 kB
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function isOwner(userId) {
return signedIn() && request.auth.uid == userId;
}
function isString(value, min, max) {
return value is string && value.size() >= min && value.size() <= max;
}
function validCrop(crop) {
return crop in ['corn', 'rice', 'soybean', 'wheat', 'tomato'];
}
function validCropList(crops) {
return crops is list &&
crops.size() >= 1 &&
crops.size() <= 5 &&
validCrop(crops[0]) &&
(crops.size() < 2 || validCrop(crops[1])) &&
(crops.size() < 3 || validCrop(crops[2])) &&
(crops.size() < 4 || validCrop(crops[3])) &&
(crops.size() < 5 || validCrop(crops[4]));
}
function validStateCode(stateCode) {
return stateCode in [
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'
];
}
function validLatLng(lat, lng) {
return (lat == null || (lat is number && lat >= -90 && lat <= 90)) &&
(lng == null || (lng is number && lng >= -180 && lng <= 180));
}
function validSeverity(severity) {
return severity in ['low', 'medium', 'high'];
}
function validCropTroubleLocation(location) {
return location is map &&
location.keys().hasOnly(['lat', 'lng', 'stateCode', 'generalArea', 'precision']) &&
validLatLng(location.lat, location.lng) &&
validStateCode(location.stateCode) &&
isString(location.generalArea, 2, 120) &&
location.precision in ['approximate', 'county_state', 'state'];
}
function validCropTroubleCreate() {
return request.resource.data.keys().hasOnly([
'userId', 'farmId', 'crop', 'issueType', 'disease', 'severity', 'description',
'location', 'photoUrl', 'photoPath', 'photoShared', 'createdAt', 'status',
'seeingTooCount', 'moderationCount'
]) &&
request.resource.data.userId == request.auth.uid &&
(request.resource.data.farmId == null || request.resource.data.farmId is string) &&
(request.resource.data.farmId == null || isFarmMember(request.resource.data.farmId)) &&
validCrop(request.resource.data.crop) &&
isString(request.resource.data.issueType, 1, 120) &&
request.resource.data.disease == request.resource.data.issueType &&
validSeverity(request.resource.data.severity) &&
request.resource.data.description is string &&
request.resource.data.description.size() <= 700 &&
validCropTroubleLocation(request.resource.data.location) &&
(
(
request.resource.data.photoShared == false &&
request.resource.data.photoUrl == null &&
request.resource.data.photoPath == null
) ||
(
request.resource.data.photoShared == true &&
isString(request.resource.data.photoUrl, 1, 2048) &&
request.resource.data.photoPath == 'cropTroubleReports/' + request.resource.id + '/shared-photo'
)
) &&
request.resource.data.createdAt == request.time &&
request.resource.data.status == 'new' &&
request.resource.data.seeingTooCount == 0 &&
request.resource.data.moderationCount == 0 &&
getAfter(/databases/$(database)/documents/reportRateLimits/$(request.auth.uid)).data.lastReportAt == request.time &&
(
!exists(/databases/$(database)/documents/reportRateLimits/$(request.auth.uid)) ||
get(/databases/$(database)/documents/reportRateLimits/$(request.auth.uid)).data.lastReportAt <
request.time - duration.value(10, 'm')
);
}
function validFarmCreate() {
return request.resource.data.keys().hasOnly([
'name', 'ownerId', 'joinCode', 'address', 'lat', 'lng', 'stateCode',
'crops', 'acreage', 'verificationStatus', 'createdAt'
]) &&
request.resource.data.ownerId == request.auth.uid &&
isString(request.resource.data.name, 1, 120) &&
isString(request.resource.data.address, 1, 240) &&
request.resource.data.joinCode.matches('^[A-Z2-9]{6}$') &&
validStateCode(request.resource.data.stateCode) &&
validCropList(request.resource.data.crops) &&
validLatLng(request.resource.data.lat, request.resource.data.lng) &&
(request.resource.data.acreage == null ||
(request.resource.data.acreage is number &&
request.resource.data.acreage >= 0 &&
request.resource.data.acreage <= 1000000)) &&
request.resource.data.verificationStatus == 'unverified' &&
request.resource.data.createdAt == request.time;
}
function memberId(farmId, userId) {
return farmId + '_' + userId;
}
function requestId(farmId, userId) {
return farmId + '_' + userId;
}
function isFarmMember(farmId) {
return signedIn() &&
exists(/databases/$(database)/documents/farmMembers/$(memberId(farmId, request.auth.uid)));
}
function isFarmOwner(farmId) {
return signedIn() &&
get(/databases/$(database)/documents/farms/$(farmId)).data.ownerId == request.auth.uid;
}
function joinCodeAllowsMembership(data) {
return data.joinCodeUsed is string &&
exists(/databases/$(database)/documents/farmJoinCodes/$(data.joinCodeUsed)) &&
get(/databases/$(database)/documents/farmJoinCodes/$(data.joinCodeUsed)).data.farmId == data.farmId;
}
function approvedRequestAllowsMembership(data) {
return data.approvedRequestId is string &&
exists(/databases/$(database)/documents/farmAccessRequests/$(data.approvedRequestId)) &&
getAfter(/databases/$(database)/documents/farmAccessRequests/$(data.approvedRequestId)).data.farmId == data.farmId &&
getAfter(/databases/$(database)/documents/farmAccessRequests/$(data.approvedRequestId)).data.requesterId == data.userId &&
getAfter(/databases/$(database)/documents/farmAccessRequests/$(data.approvedRequestId)).data.ownerId == request.auth.uid &&
getAfter(/databases/$(database)/documents/farmAccessRequests/$(data.approvedRequestId)).data.status == 'approved';
}
function validFarmSearchIndex() {
return request.resource.data.keys().hasOnly([
'farmId', 'ownerId', 'name', 'address', 'stateCode', 'crops', 'searchTokens', 'updatedAt'
]) &&
request.resource.data.farmId is string &&
request.resource.data.ownerId == request.auth.uid &&
isString(request.resource.data.name, 1, 120) &&
isString(request.resource.data.address, 1, 240) &&
validStateCode(request.resource.data.stateCode) &&
validCropList(request.resource.data.crops) &&
request.resource.data.searchTokens is list &&
request.resource.data.searchTokens.size() >= 1 &&
request.resource.data.searchTokens.size() <= 40 &&
request.resource.data.updatedAt == request.time;
}
function validAccessRequestCreate(farmId, requesterId) {
return request.resource.data.keys().hasOnly([
'farmId', 'requesterId', 'ownerId', 'status', 'farmName', 'farmStateCode',
'farmAddress', 'requestedAt', 'requestExpiresAt'
]) &&
farmId == request.resource.data.farmId &&
requesterId == request.resource.data.requesterId &&
requesterId == request.auth.uid &&
request.resource.data.status == 'pending' &&
request.resource.data.requestedAt == request.time &&
request.resource.data.requestExpiresAt is timestamp &&
request.resource.data.requestExpiresAt > request.time &&
request.resource.data.requestExpiresAt <= request.time + duration.value(30, 'd') &&
!exists(/databases/$(database)/documents/farmMembers/$(memberId(farmId, requesterId))) &&
exists(/databases/$(database)/documents/farmSearchIndex/$(farmId)) &&
get(/databases/$(database)/documents/farmSearchIndex/$(farmId)).data.ownerId == request.resource.data.ownerId &&
get(/databases/$(database)/documents/farmSearchIndex/$(farmId)).data.name == request.resource.data.farmName &&
get(/databases/$(database)/documents/farmSearchIndex/$(farmId)).data.stateCode == request.resource.data.farmStateCode &&
get(/databases/$(database)/documents/farmSearchIndex/$(farmId)).data.address == request.resource.data.farmAddress &&
request.resource.data.ownerId != request.auth.uid;
}
match /users/{userId} {
allow read: if isOwner(userId);
allow create: if isOwner(userId) &&
request.resource.data.keys().hasOnly(['name', 'email', 'emailVerified', 'createdAt']) &&
isString(request.resource.data.name, 1, 100) &&
isString(request.resource.data.email, 3, 320) &&
request.resource.data.emailVerified is bool &&
request.resource.data.createdAt == request.time;
allow update: if isOwner(userId) &&
request.resource.data.diff(resource.data).changedKeys().hasOnly(['name', 'email', 'emailVerified']) &&
isString(request.resource.data.name, 1, 100) &&
isString(request.resource.data.email, 3, 320) &&
request.resource.data.emailVerified is bool;
allow delete: if false;
}
match /farms/{farmId} {
allow read: if isFarmMember(farmId);
allow create: if signedIn() && validFarmCreate();
allow update: if isFarmOwner(farmId) &&
request.resource.data.diff(resource.data).changedKeys().hasOnly(['joinCode', 'joinCodeUpdatedAt']) &&
request.resource.data.joinCode.matches('^[A-Z2-9]{6}$') &&
request.resource.data.joinCodeUpdatedAt == request.time &&
existsAfter(/databases/$(database)/documents/farmJoinCodes/$(request.resource.data.joinCode)) &&
getAfter(/databases/$(database)/documents/farmJoinCodes/$(request.resource.data.joinCode)).data.farmId == farmId &&
getAfter(/databases/$(database)/documents/farmJoinCodes/$(request.resource.data.joinCode)).data.createdBy == request.auth.uid;
allow delete: if false;
}
match /farmJoinCodes/{joinCode} {
allow get: if signedIn() && joinCode.matches('^[A-Z2-9]{6}$');
allow list: if false;
allow create: if signedIn() &&
joinCode.matches('^[A-Z2-9]{6}$') &&
request.resource.data.keys().hasOnly(['farmId', 'createdBy', 'createdAt']) &&
request.resource.data.createdBy == request.auth.uid &&
request.resource.data.createdAt == request.time &&
getAfter(/databases/$(database)/documents/farms/$(request.resource.data.farmId)).data.ownerId == request.auth.uid;
allow update: if false;
allow delete: if signedIn() &&
resource.data.farmId is string &&
get(/databases/$(database)/documents/farms/$(resource.data.farmId)).data.ownerId == request.auth.uid;
}
match /farmSearchIndex/{farmId} {
allow read: if signedIn();
allow create, update: if signedIn() &&
farmId == request.resource.data.farmId &&
validFarmSearchIndex() &&
getAfter(/databases/$(database)/documents/farms/$(farmId)).data.ownerId == request.auth.uid;
allow delete: if false;
}
match /farmAccessRequests/{accessRequestId} {
allow read: if signedIn() &&
(resource.data.requesterId == request.auth.uid || resource.data.ownerId == request.auth.uid);
allow create: if signedIn() &&
accessRequestId == requestId(request.resource.data.farmId, request.resource.data.requesterId) &&
validAccessRequestCreate(request.resource.data.farmId, request.resource.data.requesterId);
allow update: if signedIn() &&
(
(
resource.data.ownerId == request.auth.uid &&
resource.data.status == 'pending' &&
request.resource.data.diff(resource.data).changedKeys().hasOnly(['status', 'resolvedBy', 'resolvedAt']) &&
request.resource.data.status in ['approved', 'denied'] &&
request.resource.data.resolvedBy == request.auth.uid &&
request.resource.data.resolvedAt == request.time
) ||
(
resource.data.requesterId == request.auth.uid &&
resource.data.status == 'pending' &&
request.time > resource.data.requestExpiresAt &&
request.resource.data.diff(resource.data).changedKeys().hasOnly(['status']) &&
request.resource.data.status == 'expired'
)
);
allow delete: if false;
}
match /farmMembers/{memberDocId} {
allow read: if signedIn() && resource.data.userId == request.auth.uid;
allow create: if signedIn() &&
memberDocId == memberId(request.resource.data.farmId, request.resource.data.userId) &&
request.resource.data.keys().hasOnly(['farmId', 'userId', 'role', 'joinedAt', 'joinCodeUsed', 'approvedRequestId']) &&
request.resource.data.joinedAt == request.time &&
(
(
request.resource.data.role == 'owner' &&
request.resource.data.userId == request.auth.uid &&
!('joinCodeUsed' in request.resource.data) &&
!('approvedRequestId' in request.resource.data) &&
getAfter(/databases/$(database)/documents/farms/$(request.resource.data.farmId)).data.ownerId == request.auth.uid
) ||
(
request.resource.data.role == 'member' &&
request.resource.data.userId == request.auth.uid &&
!('approvedRequestId' in request.resource.data) &&
joinCodeAllowsMembership(request.resource.data)
) ||
(
request.resource.data.role == 'member' &&
isFarmOwner(request.resource.data.farmId) &&
!('joinCodeUsed' in request.resource.data) &&
approvedRequestAllowsMembership(request.resource.data)
)
);
allow update: if signedIn() &&
resource.data.userId == request.auth.uid &&
request.resource.data.diff(resource.data).changedKeys().hasOnly(['joinedAt']);
allow delete: if signedIn() &&
resource.data.userId == request.auth.uid &&
resource.data.role != 'owner';
}
match /diagnoses/{diagnosisId} {
allow read: if signedIn() && resource.data.userId == request.auth.uid;
allow create: if signedIn() &&
request.resource.data.keys().hasOnly(['userId', 'farmId', 'crop', 'disease', 'confidence', 'detectedAt']) &&
request.resource.data.userId == request.auth.uid &&
isFarmMember(request.resource.data.farmId) &&
validCrop(request.resource.data.crop) &&
isString(request.resource.data.disease, 1, 200) &&
request.resource.data.confidence is number &&
request.resource.data.confidence >= 0 &&
request.resource.data.confidence <= 100 &&
request.resource.data.detectedAt == request.time;
allow update, delete: if false;
}
match /reportRateLimits/{userId} {
allow read: if isOwner(userId);
allow create, update: if isOwner(userId) &&
request.resource.data.keys().hasOnly(['userId', 'lastReportAt']) &&
request.resource.data.userId == request.auth.uid &&
request.resource.data.lastReportAt == request.time;
allow delete: if false;
}
match /cropTroubleReports/{reportId} {
allow read: if signedIn();
allow create: if signedIn() && validCropTroubleCreate();
allow update: if signedIn() && (
(
resource.data.userId == request.auth.uid &&
request.resource.data.diff(resource.data).changedKeys().hasOnly(['status']) &&
request.resource.data.status in ['confirmed', 'resolved']
) ||
(
request.resource.data.diff(resource.data).changedKeys().hasOnly(['seeingTooCount']) &&
request.resource.data.seeingTooCount == resource.data.seeingTooCount + 1 &&
!exists(/databases/$(database)/documents/cropTroubleReports/$(reportId)/seeingToo/$(request.auth.uid)) &&
getAfter(/databases/$(database)/documents/cropTroubleReports/$(reportId)/seeingToo/$(request.auth.uid)).data.userId == request.auth.uid
) ||
(
request.resource.data.diff(resource.data).changedKeys().hasOnly(['moderationCount']) &&
request.resource.data.moderationCount == resource.data.moderationCount + 1 &&
!exists(/databases/$(database)/documents/cropTroubleReports/$(reportId)/moderationReports/$(request.auth.uid)) &&
getAfter(/databases/$(database)/documents/cropTroubleReports/$(reportId)/moderationReports/$(request.auth.uid)).data.userId == request.auth.uid
)
);
allow delete: if signedIn() && resource.data.userId == request.auth.uid;
match /seeingToo/{userId} {
allow read: if signedIn();
allow create: if isOwner(userId) &&
request.resource.data.keys().hasOnly(['userId', 'createdAt']) &&
request.resource.data.userId == request.auth.uid &&
request.resource.data.createdAt == request.time;
allow update, delete: if false;
}
match /moderationReports/{userId} {
allow read: if signedIn() && userId == request.auth.uid;
allow create: if isOwner(userId) &&
request.resource.data.keys().hasOnly(['userId', 'reason', 'createdAt']) &&
request.resource.data.userId == request.auth.uid &&
isString(request.resource.data.reason, 1, 80) &&
request.resource.data.createdAt == request.time;
allow update, delete: if false;
}
}
match /{document=**} {
allow read, write: if false;
}
}
}