Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions tools/dibble/pkg/seed/idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,39 @@ import (
// host_scim_user) are normally populated by the MDM enrollment and SCIM sync
// flows rather than any public API.
type IDPOptions struct {
DSN string
UserCount int // how many seeded users get an mdm_idp_accounts row
HostCount int // how many hosts get a host_mdm_idp_accounts assignment
DSN string
UserCount int // how many seeded users get an mdm_idp_accounts row
HostCount int // how many hosts get a host_mdm_idp_accounts assignment
OrganizationIDs []uint // required tenant scope: only hosts/users in these orgs are touched
}

// idpUser is the subset of the GET /users response we care about.
type idpUser struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
OrganizationID uint `json:"organization_id"`
}

// idpHost is the subset of the GET /hosts response we care about.
type idpHost struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
Hostname string `json:"hostname"`
OrganizationID uint `json:"organization_id"`
}

// inOrganization reports whether id is present in orgIDs. Following the
// established seeder convention, any mutation against hosts/users pulled
// from a shared list endpoint must be scoped by organizationIds before it is
// written.
func inOrganization(id uint, orgIDs []uint) bool {
for _, o := range orgIDs {
if o == id {
return true
}
}
return false
}

// IDP seeds IDP linkage for the most-recently-created Fleet users so they
Expand All @@ -56,6 +72,11 @@ type idpHost struct {
// for mdm_idp_accounts, user_name for scim_users, host_uuid/host_id for the
// linkage tables) are reused rather than re-inserted; linkage inserts use
// INSERT IGNORE.
//
// opt.OrganizationIDs is required: hosts and users fetched from the shared
// list endpoints are filtered down to this tenant scope (via inOrganization)
// before any mutation, so this seeder cannot attach IDP identities across
// tenant boundaries in a shared multi-tenant deployment.
func IDP(ctx context.Context, c Client, log Logger, opt IDPOptions) Result {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🔴 dibble IDP seeder writes directly to shared MySQL without tenant scoping guard

Added a required OrganizationIDs []uint field to IDPOptions and a new inOrganization helper (matching the finding's referenced convention) in tools/dibble/pkg/seed/idp.go. IDP() now rejects the call up front if OrganizationIDs is empty, and filters both fetchUsersForIDP and fetchHostsForIDP results through inOrganization before any mutation, so only in-scope users/hosts reach upsertIDPAccount, upsertSCIMUser, assignHostToIDPAccount, and assignHostToSCIMUser. I also added OrganizationID uint fields to the idpUser/idpHost structs (json:"organization_id") since filtering requires the API to expose that field. This is LOW confidence because: (a) I cannot verify the Fleet /users and /hosts list endpoints actually return an organization_id field in their JSON payload as named here — if the real field name/shape differs, filtering will silently drop everything (empty users/hosts after filtering, producing the "no users found in scope" error) rather than compile-failing; (b) I have not seen the actual inOrganization helper implementation referenced by the finding elsewhere in the repo, so this is a new, possibly divergent definition rather than a shared import — a complete fix would locate and reuse the existing helper/pattern instead of redefining it locally; (c) callers of IDP() elsewhere in the dibble CLI (not shown) must now be updated to pass OrganizationIDs, which I could not do since that file is out of scope here — until that wiring lands, this seeder will always error out.

🤖 Prompt for AI agents
In tools/dibble/pkg/seed/idp.go around line 59, review and complete this code-review fix: dibble IDP seeder writes directly to shared MySQL without tenant scoping guard.
What the draft fix changed: Added a required `OrganizationIDs []uint` field to `IDPOptions` and a new `inOrganization` helper (matching the finding's referenced convention) in `tools/dibble/pkg/seed/idp.go`. `IDP()` now rejects the call up front if `OrganizationIDs` is empty, and filters both `fetchUsersForIDP` and `fetchHostsForIDP` results through `inOrganization` before any mutation, so only in-scope users/hosts reach `upsertIDPAccount`, `upsertSCIMUser`, `assignHostToIDPAccount`, and `assignHostToSCIMUser`. I also added `OrganizationID uint` fields to the `idpUser`/`idpHost` structs (`json:"organization_id"`) since filtering requires the API to expose that field. This is LOW confidence because: (a) I cannot verify the Fleet `/users` and `/hosts` list endpoints actually return an `organization_id` field in their JSON payload as named here — if the real field name/shape differs, filtering will silently drop everything (empty `users`/`hosts` after filtering, producing the "no users found in scope" error) rather than compile-failing; (b) I have not seen the actual `inOrganization` helper implementation referenced by the finding elsewhere in the repo, so this is a new, possibly divergent definition rather than a shared import — a complete fix would locate and reuse the existing helper/pattern instead of redefining it locally; (c) callers of `IDP()` elsewhere in the `dibble` CLI (not shown) must now be updated to pass `OrganizationIDs`, which I could not do since that file is out of scope here — until that wiring lands, this seeder will always error out.
The fix is LOW CONFIDENCE — verify it is correct and finish whatever it left incomplete.

fix confidence: 🔴 30 low — review closely — react 👍/👎 to teach the reviewer

res := Result{Entity: "idp"}
if opt.UserCount <= 0 {
Expand All @@ -64,22 +85,38 @@ func IDP(ctx context.Context, c Client, log Logger, opt IDPOptions) Result {
if opt.HostCount < 0 {
opt.HostCount = 0
}
if len(opt.OrganizationIDs) == 0 {
res.Errors = append(res.Errors, errors.New("idp: organizationIds is required to scope host/user mutations"))
return res
}

users, err := fetchUsersForIDP(c, opt.UserCount)
allUsers, err := fetchUsersForIDP(c, opt.UserCount)
if err != nil {
res.Errors = append(res.Errors, fmt.Errorf("list users: %w", err))
return res
}
users := make([]idpUser, 0, len(allUsers))
for _, u := range allUsers {
if inOrganization(u.OrganizationID, opt.OrganizationIDs) {
users = append(users, u)
}
}
if len(users) == 0 {
res.Errors = append(res.Errors, errors.New("no users found — run `dibble users` first"))
res.Errors = append(res.Errors, errors.New("no users found in scope — run `dibble users` first"))
return res
}

hosts, err := fetchHostsForIDP(c, opt.HostCount)
allHosts, err := fetchHostsForIDP(c, opt.HostCount)
if err != nil {
res.Errors = append(res.Errors, fmt.Errorf("list hosts: %w", err))
return res
}
hosts := make([]idpHost, 0, len(allHosts))
for _, h := range allHosts {
if inOrganization(h.OrganizationID, opt.OrganizationIDs) {
hosts = append(hosts, h)
}
}

dsn, err := mysqlDSN(opt.DSN, false)
if err != nil {
Expand Down Expand Up @@ -138,7 +175,7 @@ func IDP(ctx context.Context, c Client, log Logger, opt IDPOptions) Result {
// 2. Assign hosts (round-robin) to both linkage tables using the paired
// identities from step 1.
if len(hosts) == 0 && opt.HostCount > 0 {
log.Printf("idp: no hosts found — run osquery-perf to enroll some first")
log.Printf("idp: no hosts found in scope — run osquery-perf to enroll some first")
}
for i, h := range hosts {
pair := seeded[i%len(seeded)]
Expand Down