Skip to content

BYOD permissions [10/N]: Controls > OS Settings UI card#46136

Open
georgekarrv wants to merge 28 commits into
gkarr-23242-byod-permissions-dynamicfrom
gkarr-23242-byod-permissions-dynamic-10
Open

BYOD permissions [10/N]: Controls > OS Settings UI card#46136
georgekarrv wants to merge 28 commits into
gkarr-23242-byod-permissions-dynamicfrom
gkarr-23242-byod-permissions-dynamic-10

Conversation

@georgekarrv
Copy link
Copy Markdown
Member

Stacked PR 10 of N for #23242. First frontend PR in the stack. Targets feature branch `gkarr-23242-byod-permissions-dynamic`. Stacks on PRs 1/N–9/N.

Summary

Adds a new "BYOD permissions" section under Controls > OS Settings with two checkboxes — "Allow wipe" and "Allow lock" — for limiting what Fleet can do to manually enrolled (BYOD) Apple hosts in a fleet.

Files

  • `frontend/pages/ManageControlsPage/OSSettings/cards/BYODPermissions/`
    • `BYODPermissions.tsx` — main component
    • `_styles.scss`
    • `index.ts`
  • `OSSettingsNavItems.tsx` — adds the sidebar entry after "Passwords"
  • `router/paths.ts` — `CONTROLS_BYOD_PERMISSIONS` route
  • `interfaces/config.ts` + `interfaces/team.ts` — `allow_byod_wipe` / `allow_byod_lock` TS types

Component behavior

Mirrors the Passwords card pattern:

  • Premium-only via ``
  • MDM-enabled empty state with a "Turn on" CTA when MDM isn't configured
  • Reads from team config when a fleet is selected, otherwise falls back to global AppConfig
  • Save calls `teamsAPI.updateConfig({ mdm: { allow_byod_wipe, allow_byod_lock } }, teamId)` or `configAPI.update({ mdm: ... })` for "no fleet"
  • GitOps mode disables the save button via `GitOpsModeTooltipWrapper`

Copy

The page description warns users the setting is asymmetric:

Re-enabling a permission later does not restore it on already-enrolled hosts — they would need to re-enroll.

This matches the Apple monotonic-narrowing rule encoded in PRs 4–9 of this stack.

Test plan

  • `npx tsc --noEmit` clean
  • `npx eslint` clean on changed files
  • Reviewer: open Controls > OS Settings > BYOD permissions; verify checkboxes default to `true`, save persists, GitOps mode disables save, premium gate fires on free tier
  • Reviewer: confirm the sidebar entry copy ("BYOD permissions") matches Figma

What's next

  • 11/N: Frontend host details — consume `wipe_allowed` / `lock_allowed` from PR-8's payload to disable the Wipe/Lock action buttons + tooltip on the host details page.

Backend types and defaults for the BYOD permissions feature. No
behavior change yet — wipe/lock are not gated by these flags in this
PR. New fields default to true everywhere they could otherwise be
read as the zero value, preserving today's behavior.

- AppConfig.MDM: new optjson.Bool fields, default true via MarshalJSON
- TeamMDM (bool) / TeamPayloadMDM / TeamSpecMDM (optjson.Bool):
  symmetric fields with the same default-true semantics
- ee Service.NewTeam: initialize new teams with both flags true
- ModifyTeam (payload) + team-spec apply (full and incremental):
  honor incoming values when set
- generate-gitops: include both keys in TeamMDM seeded from AppConfig
  and in the result map emitted per team
- GitOpsControls: new keys for gitops yaml round-trip; client.go
  mapping defaults to true if the gitops file omits them
- AppConfig YAML/JSON fixtures: add allow_byod_wipe/allow_byod_lock = true
  (AppConfig.MarshalJSON defaults absent → true on API output).
- TeamMDM (per-fleet) JSON fixtures: keep new fields out unless the test
  data explicitly creates a team with them. team1/team2 test fixtures in
  get_test.go and generate_gitops_test.go are updated to set true at the
  source so expected output matches.
- macosSetupExpectedTeam*.yml: new fields set to false (savedTeam mock
  starts as zero-value and the spec doesn't override).
- generateGitops fixtures: new keys appear alphabetically at top of
  controls block; teamConfig.json / appConfig.json seeded with true.
- apply_test.go: assert.Equal for team2 (newly created via spec) expects
  true (createTeamFromSpec defaults true); assertions for team1/savedTeam
  (existing teams updated via spec) keep false.
Tracks the AccessRights integer last delivered to each BYOD Apple host
enrollment profile. Apple MDM profile-replacements cannot widen access
rights, only narrow them; SCEP/ACME renewal will use this table as the
floor when computing new rights.

- Migration 20260523111410: creates the table (host_id PK, access_rights,
  delivered_at) and backfills existing non-DEP-enrolled hosts at 8191
  (the value every enrollment has carried since forever).
- HostMDMApplePermissions type added to server/fleet/hosts.go.
- Datastore interface: GetHostMDMAppleEnrollmentPermissions +
  SetHostMDMAppleEnrollmentPermissions.
- MySQL implementations + regenerated mock.
- Drop FOREIGN KEY hosts(id) from the new table per
  handbook/engineering/scaling-fleet.md (avoid InnoDB locking on the
  hosts table). Test CI explicitly fails on host_id FKs.
- Add host_mdm_apple_enrollment_permissions to hostRefs in
  server/datastore/mysql/hosts.go so rows are cleaned up when a host
  is deleted (the manual cleanup that replaces FK cascade).
- Update migration test: drop the FK-cascade assertion, add an upsert
  assertion covering the ON DUPLICATE KEY UPDATE path used by the
  datastore method.
- Regenerated schema.sql via make test-schema (includes both the new
  table and the allow_byod_wipe/lock keys seeded in app_config_json
  from PR #1; PR #1 will land its app_config_json portion separately).
@georgekarrv georgekarrv requested review from a team as code owners May 23, 2026 22:03
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

❌ Patch coverage is 79.68254% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.84%. Comparing base (6f94f40) to head (a3e82b2).

Files with missing lines Patch % Lines
server/service/apple_mdm.go 75.65% 20 Missing and 17 partials ⚠️
ee/server/service/hosts.go 78.94% 4 Missing and 4 partials ⚠️
...0523111410_AddHostMDMAppleEnrollmentPermissions.go 77.77% 4 Missing and 2 partials ⚠️
...bles/20260524120459_BackfillTeamBYODPermissions.go 70.00% 4 Missing and 2 partials ⚠️
server/service/client.go 50.00% 2 Missing and 2 partials ⚠️
server/service/hosts.go 60.00% 1 Missing and 1 partial ⚠️
server/datastore/mysql/hosts.go 94.44% 1 Missing ⚠️
Additional details and impacted files
@@                           Coverage Diff                            @@
##           gkarr-23242-byod-permissions-dynamic   #46136      +/-   ##
========================================================================
+ Coverage                                 66.82%   66.84%   +0.01%     
========================================================================
  Files                                      2754     2756       +2     
  Lines                                    220172   220477     +305     
  Branches                                  11042    10914     -128     
========================================================================
+ Hits                                     147136   147379     +243     
- Misses                                    59742    59777      +35     
- Partials                                  13294    13321      +27     
Flag Coverage Δ
backend 68.65% <79.68%> (+0.01%) ⬆️
frontend 55.80% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@georgekarrv georgekarrv force-pushed the gkarr-23242-byod-permissions-dynamic-10 branch from a96ff82 to 84ea89d Compare May 23, 2026 22:20
@georgekarrv georgekarrv force-pushed the gkarr-23242-byod-permissions-dynamic-10 branch from 84ea89d to 988da69 Compare May 23, 2026 22:50
@georgekarrv georgekarrv force-pushed the gkarr-23242-byod-permissions-dynamic-10 branch from 988da69 to da33a5d Compare May 23, 2026 23:43
@georgekarrv georgekarrv force-pushed the gkarr-23242-byod-permissions-dynamic-10 branch from da33a5d to aaf5727 Compare May 24, 2026 00:05
Two review-driven fixes to PR-2:

- Existing migration (20260523111410): the backfill SELECT had no
  platform predicate, so any Windows host with host_mdm.enrolled=1
  AND installed_from_dep=0 (the common case — only AAD/Autopilot
  sets installed_from_dep=1 on Windows) would land in this
  Apple-specific table. Add JOIN hosts ... WHERE platform IN
  ('darwin','ios','ipados'). Added a Windows-host case to the
  existing test to assert the exclusion.

- New migration (20260524120459_BackfillTeamBYODPermissions):
  TeamMDM.AllowBYODWipe/Lock are plain bool, and TeamMDMConfig
  decodes the stored JSON without defaults. Pre-PR teams' configs
  lack both keys and would silently decode to false the moment
  PR-7's BYOD gate goes live, blocking every Wipe/Lock on every
  manually-enrolled Apple host on every pre-existing team. Backfill
  both keys to true (matching the new-team initializer in
  ee/server/service/teams.go and the AppConfig MarshalJSON default)
  via JSON_SET ... WHERE JSON_EXTRACT IS NULL so any explicit values
  (e.g. from a gitops apply between PR-1's land and this migration's
  run) are preserved. Test covers absent-keys backfill, explicit
  false preserved, and partial set (one key present, other missing).

Regenerated server/datastore/mysql/schema.sql via make test-schema.
Templates the AccessRights integer in the SCEP-backed manual enrollment
profile and the ACME enrollment profile, instead of hard-coding 8191.
Callers pass the value explicitly; for now they all pass
apple_mdm.MDMAccessRightAll so behavior is unchanged. PR-4 will compute
the value from the host's stored permissions intersected with the
owning fleet's BYOD config.

- New constants in server/mdm/apple/apple_mdm.go:
    MDMAccessRightAll         = 8191
    MDMAccessRightDeviceLock  = 512  (bit 9)
    MDMAccessRightDeviceErase = 1024 (bit 10)
- New helper AppleEnrollmentAccessRights(allowWipe, allowLock bool) int
  starts from MDMAccessRightAll and strips bits per the flags.
- enrollmentProfileMobileconfigTemplate and
  acmeEnrollmentProfileMobileconfigTemplate now take {{ .AccessRights }}.
  The account-driven user-enrollment template is unchanged — it does not
  carry AccessRights (Apple's User Enrollment imposes its own limits).
- GenerateEnrollmentProfileMobileconfig and
  GenerateACMEEnrollmentProfileMobileconfig grow an accessRights int
  parameter; all 8 production call sites in server/service and
  ee/server/service now pass apple_mdm.MDMAccessRightAll.
- Tests: TestAppleEnrollmentAccessRights covers all four
  (wipe, lock) combinations. TestGenerateEnrollmentProfileMobileconfig_AccessRights
  parses the generated mobileconfig and asserts the templated bitmask
  matches what was passed in.
Pure function that computes the AccessRights to bake into a host's
next-delivered Apple enrollment profile. Implements the monotonic
narrowing rule Apple enforces on profile-replacement:

    new_rights = stored_rights AND fleet_ceiling

For initial enrollment (storedRights == nil) the result is the fleet
ceiling. For renewals the bitwise AND ensures Fleet can never request
more rights than were previously installed — which Apple would reject.

No call sites are wired yet. PR-5 will wire it into the SCEP/ACME
profile-serving code paths and persist the chosen value via the
host_mdm_apple_enrollment_permissions table introduced in PR-2.

Tests cover both worked examples from gk/23242-plan.md:
- Example 1: stored=8191, fleet narrows to lock-only → 7679
- Example 2: stored=6655 (no wipe/lock at enrollment), fleet now
  allows both → still 6655 (AND with 8191 is no-op; Apple would
  reject widening)
Service-layer helper that combines PR-4's pure narrowing function with
DB lookups. Given a teamID and optional hostID it:

1. Loads the team's BYOD permissions (or global AppConfig when teamID
   is nil) to derive the fleet ceiling.
2. For hostID != 0, loads the host's previously-delivered AccessRights
   from host_mdm_apple_enrollment_permissions (PR-2's table).
3. Returns the bitwise AND of the two (or just the ceiling for initial
   enrollments where no row exists yet).

Callers are responsible for short-circuiting non-BYOD cases (ADE-enrolled
hosts, non-Apple platforms) before calling this — those pass
apple_mdm.MDMAccessRightAll directly to the profile generator.

Also adds fleetBYODPermissions(teamID) helper used internally.

Still no behavior change: nothing calls these yet. PR-6 will wire them
into the SCEP/ACME profile-serving endpoints + persist via
SetHostMDMAppleEnrollmentPermissions after the profile is delivered.

Tests cover:
- global config path (with and without flags explicitly set)
- team config path (and that it bypasses AppConfig)
- hostID=0 skipping the stored-rights lookup
- NotFound stored-rights handled as "no floor"
- monotonic-narrowing (example 2 from the plan: re-enabling can't widen)
- error propagation from both AppConfig and the permissions table
Threads machineInfo through generateMDMAppleSCEPEnrollProfile and
generateMDMAppleACMEEnrollProfile so they can resolve the host context
(by UDID) at profile-serve time and compute the right AccessRights to
bake into the .mobileconfig.

Adds two small glue methods:

  - resolveEnrollmentAccessRightsForServe(ctx, machineInfo)
      Looks up the host by UDID via HostLiteByIdentifier. If found,
      uses its TeamID + ID to drive computeAppleEnrollmentAccessRights
      (renewal path, monotonic narrowing). If not found (initial
      enrollment with no host record yet), falls back to global config.

  - persistEnrollmentAccessRights(ctx, hostID, accessRights)
      Records what we delivered via SetHostMDMAppleEnrollmentPermissions
      so the next renewal can AND with it. Skips for hostID == 0
      (initial enrollment; persistence happens at TokenUpdate later).

Note on initial enrollment: at serve time the device hasn't yet created
its host record, so we don't know the team. We use global AppConfig.
Per-enrollment-token team resolution would be a follow-up if product
wants different defaults per team enrollment URL.

For non-BYOD cases (ADE hosts), this endpoint is not the relevant flow;
ADE enrollments go through a separate DEP-served profile path that still
passes MDMAccessRightAll.

Tests cover:
- nil machineInfo → global config + hostID=0 (no DB lookup, no persist)
- unknown UDID → global config + hostID=0
- known host with team → team config + stored rights AND
- HostLiteByIdentifier non-NotFound error propagates
- persistEnrollmentAccessRights writes only when hostID != 0
After this PR, resolveEnrollmentAccessRightsForServe looks up the host
by UDID to decide what AccessRights to bake into the served profile.
TestGetMDMAppleEnrollmentProfileByToken exercises initial-enrollment
fixtures where the host does not yet exist, so the lookup must return
NotFound (not nil) — otherwise it panics on a nil mock func.
Two enrollment-profile generation paths were still passing
apple_mdm.MDMAccessRightAll instead of threading the new BYOD
permission ceiling from PRs 4-5:

- ee/server/service/mdm.go GetMDMManualEnrollmentProfile (admin-UI
  download). The handler already loads AppConfig - use its BYOD
  flags. No host/team context at this point (the team is only known
  after TokenUpdate), so the global AppConfig ceiling applies.

- server/service/apple_mdm.go MDMAppleProcessOTAEnrollment (the
  canonical BYOD enrollment flow). enrollSecretInfo.TeamID is in
  scope and used elsewhere in the same function, so use team-level
  narrowing via the existing svc.fleetBYODPermissions helper.

Without these fixes, when an admin set allow_byod_wipe=false (or
allow_byod_lock=false), a device enrolling through either path
would install a profile with full rights and not narrow until the
first SCEP renewal (up to scepCertRenewalThresholdDays=180 days
later). Fleet's service-layer wipe/lock gate still blocked the
Fleet-issued command, but the device-level defense-in-depth the
design relies on was missing.
Adds the service-layer enforcement that makes the BYOD permissions
feature actually functional. For manually-enrolled (On (manual)) Apple
hosts, LockHost and WipeHost in the EE service now refuse the command
when the host's effective AccessRights lack the corresponding bit.

The effective rights are the bitwise AND of:
  - the host's stored AccessRights from
    host_mdm_apple_enrollment_permissions (or MDMAccessRightAll if no
    row exists yet), and
  - the owning fleet's BYOD ceiling derived from allow_byod_wipe /
    allow_byod_lock (with global AppConfig as the fallback when the
    host has no team).

ADE-enrolled hosts and non-Apple platforms are not affected — the gate
is bypassed for any enrollment status other than "On (manual)".

New helper: (svc *Service).effectiveAppleAccessRights(ctx, host).

New error messages (server/fleet/errors.go):
  - WipeNotAllowedForBYODMessage
  - LockNotAllowedForBYODMessage
Both copy is intentionally generic so it covers both possible reasons
(fleet config disallows OR host enrolled before the right was granted),
matching the UX guidance in the plan.

Tests cover:
- ceiling with/without explicit AppConfig flags
- team-level config used and AppConfig bypassed when team is set
- stored-AND-ceiling intersection
- monotonic narrowing example (stored=no-wipe, fleet=allow-all -> stays no-wipe)
- error propagation from AppConfig, TeamMDMConfig, and the permissions table
When MDM isn't configured for a host, or the host isn't connected to
Fleet MDM, the existing checks already short-circuit with appropriate
errors. Running the BYOD permission gate before those checks broke an
existing fleetctl integration test (TestMDMLockCommand: "valid macos
but only windows mdm") because the gate's enrollment-permissions
lookup ran for a host that was never going to be wipeable/lockable in
the first place — and the test's mock store doesn't wire
GetHostMDMAppleEnrollmentPermissions, so it nil-panicked.

Reorder: BYOD gate runs only after VerifyMDMAppleConfigured and the
Fleet MDM connectivity check have succeeded. Same change applied to
both LockHost and WipeHost.

Also apply gofmt to hosts_test.go.
TestMDMLockCommand / TestMDMWipeCommand exercise the lock/wipe code
paths after PR-7 added the BYOD permission gate to LockHost/WipeHost.
For a manually-enrolled Apple host that passes the MDM-configured +
connected checks, the gate calls
ds.GetHostMDMAppleEnrollmentPermissions to resolve the host's
currently-effective rights. Without a mock the call nil-panics.

Return NotFound so the gate falls back to the fleet ceiling alone
(default-true allow_byod_*) — i.e. no restriction, matching the
test's prior expectation that lock/wipe succeed on a happy-path
"valid macos" / "valid windows" fixture.
Adds two booleans to the host details MDMHostData so the frontend can
disable the wipe/lock action buttons without re-deriving the policy.

  GET /api/v1/fleet/hosts/:id  →  body.host.mdm.wipe_allowed: boolean
                                  body.host.mdm.lock_allowed: boolean

Fields are *bool with omitempty so they only render on endpoints that
populate them (host details). The list-hosts response is unchanged.

Rules (resolveHostWipeLockAllowed):
  - non-Apple host                  → both true
  - "On (automatic)" (ADE)          → both true
  - "On (personal)" (account-driven user enrollment) → both false
  - "On (manual)" BYOD              → derived from effective AccessRights
        (host's stored AccessRights AND fleet ceiling); mirrors the gate
        enforced server-side by LockHost/WipeHost in the EE service. iOS
        and iPadOS manual hosts additionally get lock_allowed=false
        because Apple MDM has no manual-iOS lock.
  - Off / not enrolled              → both false

Populated in svc.getHostDetails next to DeviceStatus / PendingAction.

Tests cover all branches including the monotonic-narrowing case from
the plan (host enrolled before wipe was enabled → wipe_allowed stays
false even when fleet config now allows it).
…lowed

getHostDetails now populates host.mdm.wipe_allowed and host.mdm.lock_allowed
(true for the non-MDM test host fixture). Two fleetctl changes follow:

- testdata/expectedHostDetailResponseJson.json
  testdata/expectedHostDetailResponseYaml.yml
  Add the new keys (both true) to the expected host detail payload.
  Keys are placed alphabetically to match the actual JSON / YAML output
  ordering.

- mdm_test.go
  TestMDMRunCommand's `HostByIdentifier` calls reach getHostDetails →
  resolveHostWipeLockAllowed → computeAppleEnrollmentAccessRights →
  ds.GetHostMDMAppleEnrollmentPermissions. The test sets up its own
  mocks (separate from setupDSMocks). Wire the new mock with NotFound
  so the gate falls back to the fleet ceiling alone (true by default).
Updates the RenewSCEPCertificates cron so each host's renewal profile
is generated with the AccessRights it should actually have right now,
rather than a hard-coded 8191.

For each host going through SCEP/ACME renewal:
1. Look up host by UUID (HostLiteByIdentifier) -> host_id, team_id.
2. Load fleet ceiling from team config (or global AppConfig).
3. Load stored AccessRights from host_mdm_apple_enrollment_permissions
   (treat NotFound as "no prior delivery").
4. Compute new_rights = ComputeAppleEnrollmentAccessRights(...) =
   stored AND ceiling.
5. Generate per-host enrollment profile with new_rights.
6. Send via renewMDMAppleEnrollmentProfile (single assoc).
7. Persist new_rights via SetHostMDMAppleEnrollmentPermissions.

Code paths updated:
- assocsWithoutRefs (was batched, now per-host)
- assocsWithRefs    (was per-host, now also computes rights + persists)
- acmeAssocsByHostUUID (was per-host, now also computes rights + persists)

Skipped (correctly):
- userDeviceAssocs (Account Driven User Enrollment — its mobileconfig
  template carries no AccessRights key; Apple's User Enrollment imposes
  its own permission ceiling).
- assocsFromMigration (uses external FLEET_SILENT_MIGRATION_ENROLLMENT_
  PROFILE which we don't control).

The previous batch optimization for assocsWithoutRefs is gone. Renewal
runs hourly with maxCertsRenewalPerRun=100 so per-host work is bounded
to ~100 profile generations per run — well within budget for SCEP cert
renewal which is rare per host (every ~6 months).

New free function: resolveRenewalAccessRights(ctx, ds, hostUUID).
New free function: loadFleetBYODPermissions(ctx, ds, teamID).
- server/service/apple_mdm.go: gofmt fixed an indented-block formula
  comment in the resolveRenewalAccessRights doc.
- server/service/apple_mdm_test.go: TestRenewSCEPCertificatesBranches
  and TestRenewACMECertificatesBranches now exercise the per-host
  AccessRights resolution added in this PR. The shared test setup
  doesn't seed host records, so wire HostLiteByIdentifierFunc to
  return NotFound. The helper then falls back to MDMAccessRightAll
  with hostID=0, which skips persistence — matching the existing
  test expectations.
The SCEP / ACME renewal cron called resolveRenewalAccessRights on
every renewing host, including ADE (DEP / Automated Device
Enrollment) hosts. When an admin set allow_byod_wipe=false or
allow_byod_lock=false in the fleet config, those settings would
silently leak into the renewal profile of supervised, corporate-
owned devices and strip Wipe / Lock from IT's toolbox.

The BYOD settings target personal enrollments by design; ADE hosts
are out of scope. Look up host_mdm at the start of the helper and
return MDMAccessRightAll when installed_from_dep is set, before
any fleet config or stored rights are consulted.

If host_mdm has no row for the host (extremely unusual at renewal
time — the cron pulls candidates from the SCEP table, not host_mdm)
fall through to the BYOD path. host_mdm DB errors propagate.

Test coverage: ADE short-circuit (verifies AppConfig / TeamMDMConfig
/ GetHostMDMAppleEnrollmentPermissions are NOT invoked), GetHostMDM
not-found fall-through, GetHostMDM error propagation. Existing
known-host tests gained GetHostMDMFunc mocks returning a non-ADE
row so they reach the BYOD path.
New "BYOD permissions" section under Controls > OS Settings with two
checkboxes — "Allow wipe" and "Allow lock" — for limiting what Fleet
can do to manually enrolled (BYOD) Apple hosts that belong to a fleet.

- Component: frontend/pages/ManageControlsPage/OSSettings/cards/
    BYODPermissions/{BYODPermissions.tsx, _styles.scss, index.ts}
  Mirrors the Passwords card structure: section header, page
  description, two checkboxes, save button, GitOps lock support, MDM-
  enabled empty state, premium feature gate.
- Route: PATHS.CONTROLS_BYOD_PERMISSIONS = /controls/os-settings/byod-permissions
- OSSettingsNavItems: appended to the sidebar after "Passwords"
- IMdmConfig + ITeam.mdm: new allow_byod_wipe / allow_byod_lock booleans
  (typed; both default to true on read)

Apply path:
- Global config (no fleet selected) -> configAPI.update({ mdm: { ... } })
- Per-fleet                          -> teamsAPI.updateConfig({ mdm: { ... } }, currentTeamId)

Both endpoints accept arbitrary JSON keys via mdm, so no service-layer
changes are needed for the round-trip.

The description copy in the card warns users about the asymmetric
nature of the setting (re-enabling does not restore on already-
enrolled hosts — they would need to re-enroll), matching the design
guidance in gk/23242-plan.md.
IMdmConfig grew two required fields in this PR; the shared test
config mock has to provide them or TypeScript compilation fails in
any test that imports the mock. Both default to true to match the
production default.
@georgekarrv georgekarrv force-pushed the gkarr-23242-byod-permissions-dynamic-10 branch from a8ea071 to a3e82b2 Compare May 24, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant