feat(web): partner-wide ("all orgs") owner option on config policy create - #2064
Merged
ToddHebebrand merged 2 commits intoJun 30, 2026
Merged
Conversation
…eate Partner-owned configuration policies (org_id NULL, partner_id set — the "all orgs" shape) were shipped backend-only in #1724/#1751: the service, routes, validators, and resolution all support `ownerScope: 'partner'`, but the create page was never wired to use it. It hardcoded `orgId: currentOrgId`, so in All-orgs scope (currentOrgId null) creation 400'd, and there was no way to make a partner-wide policy at all. Assigning an *org-owned* policy at the Partner level appears to work but doesn't: resolution filters org-owned policies to org_id = device.orgId, so it only ever reaches that one org. True all-orgs propagation requires a partner-owned policy — which you now can create. Adds an "Apply to" picker on the create page for partner-scope creators (gated on JWT scope, mirroring AlertTemplateEditor's availability picker): All organizations (partner-wide) vs a specific organization. Defaults to partner-wide when viewing All-orgs scope. Org-scope creators are unaffected (picker hidden, classic orgId payload). Backup-not-supported caveat surfaced inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iew) Review of #2064 (silent-failure-hunter + code-reviewer) found the org-scoped submit value diverged from the dropdown. `effectiveOrgId = ownerOrgId || currentOrgId || ''` meant a partner creator focused on an org who cleared the select to the placeholder would silently POST the focused org while the UI showed nothing chosen. The org dropdown is now authoritative for partner-scope creators (no currentOrgId fallback); org-scope creators (no picker) still use their own org. Also guard the empty-org case inside onSubmit, not just on the disabled button — the Enter key bypassed the button and POSTed `{ orgId: '' }`, surfacing only an opaque server 400 instead of an actionable message. Plus a comment-accuracy tweak: the picker follows AlertTemplateEditor's JWT-scope detection but, unlike it, shows for any partner-scope creator (not only >1 org). Adds 3 guard tests + asserts the focused-org POST body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand
deleted the
Issue-with-partner-level-config-policy-creation
branch
June 30, 2026 15:48
This was referenced Jun 30, 2026
Closed
ToddHebebrand
added a commit
that referenced
this pull request
Jul 1, 2026
…ab (#2107) ## Problem The create-page **"Apply to" owner picker** (#2064) collided with the **Assignments tab**. Both surfaced a "partner-wide / all orgs" choice, but they control two independent axes: - **Ownership** — `configuration_policies.org_id` XOR `partner_id` (DB CHECK), set once at create. The eligibility universe. - **Assignment** — `config_policy_assignments` rows, what `resolveEffectiveConfig` actually reads. Three concrete breakages: 1. The create picker **applied nothing** — a partner-owned policy was created with zero assignments, so it resolved to **no devices** until the user separately found the Assignments tab. 2. The Assignments tab was **blind to ownership** — got `orgId=null` for partner-owned policies (breaking target lookups) and offered all 5 levels regardless. 3. **Org-owned + partner-level** validated and saved but silently applied only to the one owning org (footgun). ## Fix — ownership is the single source of truth; the tab is ownership-aware - **`crud.ts`** — creating a partner-owned policy **auto-seeds the matching partner-level assignment**, so "All organizations" applies immediately. The two inserts aren't transactional, so a non-unique seed failure **rolls back the orphan** and surfaces the error (logged with the policy id) rather than leaving a committed-but-unassigned policy; unique violations are tolerated. - **`configurationPolicy.ts`** — `validateAssignmentTarget` now **rejects org-owned policies at the Partner level** with an actionable message. - **`AssignmentsTab.tsx`** — partner-owned shows a banner + a re-assign card only when unassigned; org-owned **drops the Partner-Wide level option**. Guards against a null `orgId` in the site/group target fetch. - **`ConfigPolicyDetailPage.tsx`** — `PolicyDetail` carries `partnerId` (`orgId` now nullable), passed to the tab. - **`ConfigPolicyCreatePage.tsx`** — relabeled **"Apply to" → "Scope"**; clarified helper text. - **`aiToolsConfigPolicy.ts`** — documented the partner-level constraint in the tool description. ## Tests - New `configurationPolicy.validateAssignment.test.ts` — ownership gating (all four combinations), asserting the illegal combos short-circuit before any DB query. - `crud.test.ts` — partner auto-assign, org-owned does-not-auto-assign, unique-swallow (still 201), non-unique-rollback (500 + `deleteConfigPolicy` called). - `AssignmentsTab.test.tsx` — partner banner/no-picker, re-assign POST body (level + priority + role/OS filters, no `targetId`), card hiding, org-owned level list excludes `partner`, org-owned assign POST includes `targetId`. ## Verification `astro check` 0 errors · API `tsc` clean · eslint clean (both apps) · API 94 config-policy tests + web 14 component tests green. This is a follow-up to #2064. Reviewed via the pr-review-toolkit (code / tests / silent-failure / comments / types); all surfaced findings were addressed in this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Todd Hebebrand <todd@lanternops.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToddHebebrand
added a commit
that referenced
this pull request
Jul 1, 2026
Documentation sweep for the **v0.88.0** release (range `v0.87.0..v0.88.0`). Updates 15 existing docs pages to reflect shipped features and fixes — all `Update`s, no new pages. ## Pages updated - **Networking** — `features/integrations.mdx` (self-hosted UniFi controller: agent-mediated, no cloud key, one controller → many orgs; + cloud Site Manager accuracy, #2097/#2103), `features/discovery.mdx` (native network-device detail page + `GET /discovery/assets/:id`, #1998), `features/dns-security.mdx` (Pi-hole v6: `piholeVersion`, app password, 429 `no_seats`, #2069) - **Security & incidents** — `features/incident-response.mdx` + `reference/api.mdx` (EDR-aware Incidents feed, `GET /incidents/feed`, #2095), `features/pam.mdx` + `security/pam.mdx` + `reference/api.mdx` (SHA-256 signer **thumbprint pin** — API-only, fail-closed, XOR signer-group, #2080), `features/security.mdx` (Elastic Defend provider row, #2068) - **Patching** — `features/configuration-policies.mdx` + `features/patch-management.mdx` ("Manage Windows Update exclusively through Breeze" toggle, default OFF, `NoAutoUpdate=1`, #2079; `releaseDate` + firmware/drivers-only reject, #2116; partner-wide owner accuracy + priority-direction fix, #2064), `features/deployments.mdx` (`.exe`/`.msi` detection rules + `forceReinstall`, #2088) - **Ops workflows** — `features/maintenance-windows.mdx` (reboot-if-pending sweep, #2096), `features/ticketing.mdx` (`unknownSenderMode` + `dropUnverifiedSenders`, #2105), `features/alerts.mdx` (suppress: no-suppress-when-resolved, "Forever", expiry reaper, bulk suppress, #2110), `features/reports.mdx` (posture report now UI-selectable, #2087) ## Also - `scripts/docs-review/mapping.json` — +4 code→doc mappings (incidents→api, softwareDeployment→deployments, new `maintenanceRebootWorker.ts`→maintenance-windows, `devices/network` page→discovery) - `scripts/docs-review/last-reviewed.json` — tracker bumped to `v0.88.0` - **`AssignmentsTab.tsx`** — one-line fix: the policy **Priority** tooltip said "higher values override lower ones," but the resolver sorts `priority ASC` with first-wins (`featureConfigResolver.ts:207`, `configurationPolicy.ts:1328`) — i.e. **lower-number-wins**. Tooltip corrected to match. (Small code fix bundled here because it's the same priority-direction accuracy issue the docs sweep surfaced.) `pnpm --filter docs build` passes — **130 pages**, no MDX/frontmatter errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Todd Hebebrand <todd@lanternops.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two reported symptoms, one root cause:
currentOrgIdnull), the create page sent{ orgId: null }and the API rejected it ("orgId is required when partner has multiple organizations"). There was no UI to make a partner-wide policy at all.org_id = device.orgId, so the assignment only ever reaches that one org — never the other orgs under the partner.Partner-owned ("all orgs") policies were shipped backend-only in #1724/#1751 — the schema (
org_idXORpartner_id), service (createConfigPolicyacceptsownerScope: 'partner'), routes, validators, and the resolution join all support it. The create page was never wired up, hardcodingorgId: currentOrgId.Fix (frontend only — no backend/schema/migration)
ConfigPolicyCreatePage.tsxnow shows an "Apply to" picker for partner-scope creators (gated on JWT scope, mirroringAlertTemplateEditor's availability picker):{ ownerScope: 'partner' }; the server derives the partner from the caller's token (never a client value) and enforcesorgAccess: 'all'.{ orgId }payload, with an org select.Defaults to partner-wide when the user is viewing All-orgs scope, else the focused org. Org-scope creators are unaffected — picker hidden, behavior identical to before. The backup-not-supported-on-partner-wide caveat is surfaced inline.
For true cross-org propagation, the policy must be partner-owned (this PR), not an org-owned policy assigned at the partner level — the AssignmentsTab partner level was already correct for partner-owned policies.
Tests
New
ConfigPolicyCreatePage.test.tsx(5 cases): picker visible + partner-wide default in All-orgs scope; partner-wide POST omitsorgId; switching to a specific org sendsorgIdwithoutownerScope; org-default when a concrete org is focused; picker hidden + classic payload for org-scope creators. All config-policy web tests (64), theno-silent-mutationsguard,tsc, and eslint pass.🤖 Generated with Claude Code