fix(store): scope SetDomainStatus by project (IDOR); scheduler reuses DeriveStatus
handleCheck's error branch wrote last_check_status via an id-only UPDATE, so an authenticated caller's own valid project id paired with a foreign domain id in the URL could flip a stranger's domain to "error" even though Check itself is project-scoped and would 404/error out first. Add project_id to the WHERE clause (queries/domains.sql + generated db/domains.sql.go), thread projectID through Store/TenantStore/SchedStore SetDomainStatus, and pass pid from context at both call sites in handleCheck plus the scheduler. Also collapse checkDomain's inline status derivation in scheduler.go into a call to service.DeriveStatus, the same helper handleCheck already uses, so there's a single source of truth for "drift vs in_sync" instead of two copies that could drift apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
@@ -40,7 +40,9 @@ type SchedStore interface {
|
||||
TouchScheduleRun(ctx context.Context, projectID uuid.UUID, at time.Time) error
|
||||
ListDomains(ctx context.Context, projectID uuid.UUID) ([]store.Domain, error)
|
||||
GetDomainStatus(ctx context.Context, domainID uuid.UUID) (string, error)
|
||||
SetDomainStatus(ctx context.Context, domainID uuid.UUID, status string) error
|
||||
// SetDomainStatus is scoped by projectID so a foreign domain ID can never
|
||||
// have its status overwritten (IDOR-on-write) — see internal/store/tenant.go.
|
||||
SetDomainStatus(ctx context.Context, domainID, projectID uuid.UUID, status string) error
|
||||
CountDriftDomains(ctx context.Context) (int, error)
|
||||
}
|
||||
|
||||
@@ -144,12 +146,12 @@ func (s *Scheduler) checkDomain(ctx context.Context, projectID uuid.UUID, d stor
|
||||
cs, checkErr := s.checker.Check(ctx, projectID, d.ID)
|
||||
dur := time.Since(start)
|
||||
|
||||
newStatus := StatusInSync
|
||||
switch {
|
||||
case checkErr != nil:
|
||||
newStatus = StatusError
|
||||
case len(cs.Actionable()) > 0:
|
||||
newStatus = StatusDrift
|
||||
// Derive the status via the same helper the manual check handler uses
|
||||
// (internal/api/handlers.go) so both paths agree on what counts as
|
||||
// "drift" vs. "in sync" — a failed check is always "error" regardless.
|
||||
newStatus := StatusError
|
||||
if checkErr == nil {
|
||||
newStatus = service.DeriveStatus(cs)
|
||||
}
|
||||
s.metrics.ObserveCheck(newStatus, dur)
|
||||
|
||||
@@ -164,7 +166,7 @@ func (s *Scheduler) checkDomain(ctx context.Context, projectID uuid.UUID, d stor
|
||||
// check (drift or in_sync). Calling it again here would double-write
|
||||
// check_runs history for the same check.
|
||||
|
||||
if err := s.store.SetDomainStatus(ctx, d.ID, newStatus); err != nil {
|
||||
if err := s.store.SetDomainStatus(ctx, d.ID, projectID, newStatus); err != nil {
|
||||
log.Printf("scheduler: set domain status for %s failed: %v", d.ID, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user