c77ff07ce9
Remediation of the 2026-05-12/13 audits (78 findings + cluster gaps), tracked in deploy-k3s/SECURITY.md, plus fixes from two independent post-remediation reviews. Auth & sessions: - SHA-256 hashed auth-token storage (C1); prior-token cache eviction on re-login (MEDIUM-1) - local Google JWKS verification, iss/aud/exp checks (C2/C3) - constant-time login + generic errors (L1/LIVE-L11/LIVE-L13) - per-account login lockout keyed on distinct source IPs (M5/MEDIUM-3) - verified-email gating, login rate limiting (LIVE-L19, H1-H3) IAP & webhooks: - Apple/Google cross-account replay protection (C5/C6/C10/C13, H5/H6) - migrations 000003-000006 (token hashing, IAP replay, audit_log + webhook_event_log table creation, append-only audit log) Authorization & races: - file-ownership owner-OR-member fix (C7), atomic share-code join (C9/H9), device-token reassignment (C8/LOW-3) Secrets & deploy: - secrets file-mounted at /etc/honeydue/secrets, not env (F8); Redis password out of the ConfigMap (HIGH-1); B2 keys reconciled - digest-pinned images, admin ingress hardening, CSP/HSTS, /metrics lockdown; kubeconfig 0600, etcd secrets-encryption, fail2ban + unattended-upgrades at provision; secret-rotation runbook Build, vet, and the full test suite (incl. -race) pass; the goose migration chain is verified against PostgreSQL 16. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.8 KiB
Go
82 lines
2.8 KiB
Go
package services
|
|
|
|
import (
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// FileOwnershipService checks whether a user has access to a file referenced
|
|
// by URL. It queries task completion images, document files, and document
|
|
// images, resolving access through residence ownership or membership.
|
|
type FileOwnershipService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewFileOwnershipService creates a new FileOwnershipService
|
|
func NewFileOwnershipService(db *gorm.DB) *FileOwnershipService {
|
|
return &FileOwnershipService{db: db}
|
|
}
|
|
|
|
// accessibleResidenceIDs returns a subquery of residence IDs the user can
|
|
// access: residences they own (residence_residence.owner_id) UNION residences
|
|
// they are a member of (residence_residence_users).
|
|
//
|
|
// Audit C7: the previous queries joined residence_residence_users only, so a
|
|
// residence owner who was not also a member of the join table could not pass
|
|
// the ownership check for files in their own property.
|
|
func (s *FileOwnershipService) accessibleResidenceIDs(userID uint) *gorm.DB {
|
|
return s.db.Raw(`
|
|
SELECT id FROM residence_residence WHERE owner_id = ?
|
|
UNION
|
|
SELECT residence_id FROM residence_residence_users WHERE user_id = ?
|
|
`, userID, userID)
|
|
}
|
|
|
|
// IsFileOwnedByUser checks if the given file URL belongs to a record in a
|
|
// residence the user owns or is a member of.
|
|
func (s *FileOwnershipService) IsFileOwnedByUser(fileURL string, userID uint) (bool, error) {
|
|
// Task completion images: image_url -> completion -> task -> residence.
|
|
var completionImageCount int64
|
|
err := s.db.Model(&models.TaskCompletionImage{}).
|
|
Joins("JOIN task_taskcompletion ON task_taskcompletion.id = task_taskcompletionimage.completion_id").
|
|
Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
|
|
Where("task_taskcompletionimage.image_url = ?", fileURL).
|
|
Where("task_task.residence_id IN (?)", s.accessibleResidenceIDs(userID)).
|
|
Count(&completionImageCount).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if completionImageCount > 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// Document files: file_url -> document -> residence.
|
|
var documentCount int64
|
|
err = s.db.Model(&models.Document{}).
|
|
Where("task_document.file_url = ?", fileURL).
|
|
Where("task_document.residence_id IN (?)", s.accessibleResidenceIDs(userID)).
|
|
Count(&documentCount).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if documentCount > 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// Document images: image_url -> document_image -> document -> residence.
|
|
var documentImageCount int64
|
|
err = s.db.Model(&models.DocumentImage{}).
|
|
Joins("JOIN task_document ON task_document.id = task_documentimage.document_id").
|
|
Where("task_documentimage.image_url = ?", fileURL).
|
|
Where("task_document.residence_id IN (?)", s.accessibleResidenceIDs(userID)).
|
|
Count(&documentImageCount).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if documentImageCount > 0 {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|