feat(grpc): resolve org from server record for agent RPCs
Two instances of the branch's recurring bug class remained in the agent path: a client-supplied ID accepted as data, then consumed by an unscoped query. - ListMonitorsForRunner filtered on `runner` alone, and `runner` is set by the client on monitor create/update. Org A could point a monitor at org B's server_id and org B's agent would fetch and execute the check. Now org-filtered, and `runner` is validated against the caller's org on create and update. - IngestResult resolved the monitor via the unfiltered getMonitorByID using a monitor_id from the agent's request body, letting org A's agent write state and incidents into org B's monitor and fire its channels. Now rejects on org mismatch and on a monitor not assigned to the reporting agent. The in-process scheduler passes an empty orgID as a documented sentinel for the cross-org server-run sweep. Install script still emits a single shared GRPC_HOST; the agent path resolves org from the server record, never from a hostname.
This commit is contained in:
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
@@ -51,12 +52,19 @@ func ListMonitors(orgID string) ([]models.Monitor, error) {
|
||||
}
|
||||
|
||||
// ListMonitorsForRunner returns enabled monitors whose Runner matches runner.
|
||||
// Agent/scheduler path — a cross-org sweep (mirrors MarkOfflineServers), so it
|
||||
// intentionally has no org filter.
|
||||
func ListMonitorsForRunner(runner string) ([]models.Monitor, error) {
|
||||
// Runner is client-supplied at write time, so an agent fetching its own work
|
||||
// must scope by the org of its authenticated server record — otherwise another
|
||||
// org could point a monitor at that server_id and have it run their checks.
|
||||
// An empty orgID means the cross-org server-scheduler sweep (mirrors
|
||||
// MarkOfflineServers) and is only ever passed with runner == RunnerServer.
|
||||
func ListMonitorsForRunner(orgID, runner string) ([]models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("monitors").Find(ctx, bson.M{"runner": runner, "enabled": true})
|
||||
filter := bson.M{"runner": runner, "enabled": true}
|
||||
if orgID != "" {
|
||||
filter["org_id"] = orgID
|
||||
}
|
||||
cur, err := db.Col("monitors").Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,12 +106,29 @@ func getMonitorByID(monitorID string) (*models.Monitor, error) {
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// validateRunner rejects a runner that is neither the reserved server-scheduler
|
||||
// value nor a server in the org. The value is client-supplied and is later
|
||||
// consumed by an agent's own monitor fetch, so ownership has to be proven at
|
||||
// the write boundary.
|
||||
func validateRunner(orgID, runner string) error {
|
||||
if runner == "" || runner == models.RunnerServer {
|
||||
return nil
|
||||
}
|
||||
if _, err := GetServer(orgID, runner); err != nil {
|
||||
return fmt.Errorf("runner server %s not found", runner)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateMonitor(orgID string, m *models.Monitor) (*models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
if err := validateChannelIDs(orgID, m.ChannelIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRunner(orgID, m.Runner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.OrgID = orgID
|
||||
m.MonitorID = uuid.NewString()
|
||||
m.CreatedAt = time.Now()
|
||||
@@ -131,6 +156,11 @@ func UpdateMonitor(orgID, monitorID string, upd bson.M) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if runner, ok := upd["runner"].(string); ok {
|
||||
if err := validateRunner(orgID, runner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}, bson.M{"$set": upd})
|
||||
return err
|
||||
}
|
||||
@@ -191,7 +221,13 @@ func UptimeRollups(monitorID string, since time.Time) ([]models.Rollup, error) {
|
||||
// incidents on up<->down transitions, rolls up the hourly bucket, and fires
|
||||
// notifications on transition. Both the server scheduler and agent-reported
|
||||
// results funnel through here.
|
||||
func IngestResult(monitorID string, res checker.Result) error {
|
||||
//
|
||||
// monitorID is client-supplied on the agent path, so the caller passes the org
|
||||
// and runner it is authenticated as: orgID is the reporting agent's server org
|
||||
// and runner is its server_id. A result is only applied to a monitor owned by
|
||||
// that org and assigned to that runner. An empty orgID is the in-process server
|
||||
// scheduler, which passes runner == RunnerServer.
|
||||
func IngestResult(orgID, runner, monitorID string, res checker.Result) error {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
|
||||
@@ -199,6 +235,12 @@ func IngestResult(monitorID string, res checker.Result) error {
|
||||
if err != nil || m == nil {
|
||||
return err
|
||||
}
|
||||
if orgID != "" && m.OrgID != orgID {
|
||||
return fmt.Errorf("monitor %s belongs to another org", monitorID)
|
||||
}
|
||||
if m.Runner != runner {
|
||||
return fmt.Errorf("monitor %s is not run by %s", monitorID, runner)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
prev := m.State.Status
|
||||
|
||||
Reference in New Issue
Block a user