Skip to content

feat(domain-join): add interactive and zero-touch domain joining - #399

Draft
mchave3 wants to merge 88 commits into
mainfrom
feat/domain-join-218
Draft

mchave3 wants to merge 88 commits into
mainfrom
feat/domain-join-218

Conversation

@mchave3

@mchave3 mchave3 commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add on-premises Active Directory domain joining to Foundry: an Interactive mode where the technician enters credentials during deployment, and a Zero-touch mode where the media carries protected credentials. One media can serve several domains, each with its own organizational units (OUs) and join account, and both modes can place the computer in a chosen OU.

Reason

Issue #218 asks for domain joining from shared deployment media. The join runs online in installed Windows, before OOBE, so one USB serves any computer without preparing a package per device. This replaces the offline domain join approach first proposed in the issue.

Main changes

  • Foundry OSD: add a Domain Join section with Zero-touch and Interactive pages. Each lists the domains the media can join and, per domain, an optional list of OUs added through a dialog or imported from that domain through a picker, with a default domain and a default OU per domain. Listing several domains, or several OUs for a domain, makes the technician choose among them during deployment, with the default preselected; a single listed OU is always used. The lists are driven by command bars, and an OU display name can be edited. Join passwords stay with their account when the mode changes or joining is disabled. Zero-touch also stores a shared join account and optional dedicated accounts per domain, protected by the deployment password of the General page's password protection.
  • Provisioning modes: only one Autopilot or Domain Join mode can be enabled. All five pages share one confirmation that names the current and the requested mode.
  • Foundry Deploy: add a Domain join wizard step, shown before the summary when the technician has credentials to enter or a domain or OU to choose, and a matching summary category. Stage the input for installed Windows with access restricted to SYSTEM and Administrators. The debug menu offers Interactive and Zero-touch scenarios.
  • Foundry PostInstall: wait for the directory to become reachable, join with NetJoinDomain, then move an existing computer account to the chosen OU while preserving its identity. Joining, OU placement, membership, restart and credential cleanup are tracked separately.
  • Failure behavior: a failed join or placement is reported as a warning and Windows installation continues. An interrupted or uncertain join or move is never replayed. A join target must be an organizational unit; if it no longer exists at join time, the computer is joined in the domain's default location and placement is reported as failed.
  • Diagnostics: exclude the credential payload from support bundles and mask quoted and escaped secret forms in logs.
  • Tables: every table in Foundry OSD (domains, OUs, Autopilot profiles, tenant readiness, certificates, custom images and their indexes, post-installation actions) now spans its card with proportional columns. All of them can be sorted from their headers except the post-installation table, which keeps its execution order.
  • OOBE: when a join is staged with the generated answer file, hide the Microsoft account sign-in, and let PostInstall skip the account creation page once it has verified the domain membership, so setup ends on the sign-in screen without any local account. A failed or unverified join keeps that page.
  • Telemetry: report Domain Join usage on the existing osd:boot_media_finished and deploy:session_finished events (mode, domain and OU counts, default OU, shared account use, origin of the chosen domain and OU, staging status). No domain, account, OU or computer name is sent; the join outcome in installed Windows is not reported.
  • Localization: translate every Domain Join string in all 38 supported cultures, with en-US as the reference. Correct the General page title in fifteen cultures where it was mistranslated; that defect predates this pull request.

Testing

  • .\scripts\Test-FoundryFormat.ps1
  • x64 Release build and relevant tests
  • ARM64 Release build and relevant tests, when relevant
  • Manual validation

Results on x64: format check 78/78; full solution Release build with CI flags and warnings as errors, zero warnings and errors; all eight suites, 4,508 passed of 4,515, with the seven existing explicit network cases (two Bootstrap, five Deploy) not run. Two tests are removed, NegotiatedChildAcknowledgementUsesActualLaunchIdentity (Bootstrap) and AcquireAsync_AnotherProcessLeasePreventsAcquisition (Core): each starts a real powershell.exe child under a 10 to 15-second deadline and timed out on a slow ARM64 runner, on code this pull request does not touch.

Manual validation, by the maintainer in a disposable AD lab on an x64 virtual machine (Windows 11 Pro):

  • An Interactive join end to end, with the account and password typed on the Deploy step.
  • A Zero-touch join, ending on the Windows sign-in screen without any local account.
  • A join with a wrong password: reported as a warning, not retried, and setup ends on the account creation page.
  • The technician choice among several domains and among several OUs on the Deploy step.
  • A reused computer account.
  • In Foundry OSD: the OU rename, the passwords kept across a mode change, and the full-width tables.

Validation not run and reason:

  • ARM64: not built or run locally; left to CI. No physical ARM64 device was available.
  • Second domain: the lab has one domain, so a real two-domain join and the OU import against a second trusted domain are covered by unit tests only.
  • Missing OU, restart and recovery: the path where the target OU no longer exists, and recovery after an interrupted join, are covered by unit tests only.
  • Translations: checked against en-US by a second automated pass, not by native speakers.

Additional information

🤖 Generated with Claude Code

@github-actions github-actions Bot added dependencies Dependency, package, or project metadata change ci CI, automation, or workflow changes tests Test coverage or test infrastructure change ui User interface or XAML changes project: foundry Changes in the Foundry desktop application project: foundry-deploy Changes in the Foundry.Deploy deployment application project: foundry-core Changes in the Foundry.Core shared business logic library project: shared Changes in shared localization, telemetry, or utility libraries project: foundry-postinstall Changes in the Foundry.PostInstall post-installation runtime labels Oct 4, 2026
@mchave3 mchave3 self-assigned this Oct 4, 2026
@github-actions github-actions Bot removed the ci CI, automation, or workflow changes label Oct 4, 2026
mchave3 and others added 28 commits October 8, 2026 11:16
…veral domains

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The step lists the media's domains when technicians may choose and more
than one is listed; otherwise it shows the retained domain. The OU field
follows the retained domain and resets to that domain's default, while
the typed account and password are kept. Preparation decrypts the
retained domain's own payload, refuses a domain or OU that is not
listed for it, and records where the domain came from for telemetry.
The Debug scenarios list two domains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the strings of the domain list, its dialog and the command bars in
all 38 cultures, reword the messages that named this computer's domain,
and remove the strings of the cards that no longer exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profile payloads are read with unmapped members disallowed, so a profile
written before the domain list existed was rejected whole. Drop the
retired Domain Join members before the strict read; the profile loads
with an empty domain list, as the authoring file already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Interactive page hides the account inputs and saved a null account,
which erased the dedicated account Zero-touch had stored on the domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tials

Make the shared-account test involve an account two domains really
share, cover the per-domain credential rule on protected media, and
correct two comments that described removed behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WinUI.TableView keeps 32 pixels aside for the row check box when it
shares out proportional widths, which left an empty strip beside the
last column of the single-selection domains table. Apply the declared
proportions as pixel widths on resize instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switching to Interactive or switching Domain Join off erased every join
password, which with several domains meant retyping each one after a
round trip. A password now stays for as long as the configuration stores
its account, and is erased only when the account is removed or changed.
Media generation still uses passwords in Zero-touch only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An imported OU takes its directory name as display name. The OU command
bar gains Edit, which changes that name for the selected OU while keeping
its distinguished name, its default status and later imports untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an choice

Remove the two "Technicians can choose" settings. Several listed domains
now make the technician choose one, with the default preselected; a
single listed OU is always used and several make the technician choose.
A Zero-touch deployment stays free of input with one domain and at most
one OU. Profiles saved with the retired settings still load, and the two
matching telemetry properties are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anges

Drop the local-draft loader, which only forwarded to the regular one
since the catalog-mismatch tolerance was removed, and a secret-state
property that only a test read. Correct two comments that still
described a search of the authoring computer's own domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atalog

The three keys still named after the single OU catalog now say what
they hold: the OU list header, its empty state and the refused OU
entry. Values are unchanged in every culture. The Deploy test helper
that lists one OU follows the same naming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

The confirmation is about the disk and the image. The domain and OU
are already reviewed in the Domain join category of the summary page,
so the extra line is removed with its now unused format string. The
unsupported-edition warning stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ccount page

Windows client editions ignore HideLocalAccountScreen, so a joined
computer still stops on "Who's going to use this device?" unless the
answer file creates a local account. Both Domain Join pages now show a
non-blocking warning while the OOBE page configures none, and the
Deploy comment says what the two settings really do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…age warning

A lab deployment with the built-in Administrator enabled still stopped
on the Windows account creation page. Microsoft documents the page as
skipped when the answer file creates a user account, which the built-in
Administrator is not, so the warning now stays until a local account is
added and its text no longer suggests the Administrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows client still asked who will use the device after a successful
join. The post-installation runtime now marks account creation as done
(UnattendCreatedUser, validated in the lab) once it has verified the
domain membership, when Deploy requested it for the answer file Foundry
generates. A failed or unverified join keeps the page, so a local
account can still be created, and an imported answer file keeps its own
OOBE choices. The authoring warning about a missing local account is
removed, since none is needed any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply the full-width column helper, validated on the Domain Join
domains tables, to the other tables without row check boxes: custom
images and their indexes, post-installation actions and the Autopilot
tenant readiness table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A configuration file edited by hand could carry null where a list is
expected. Validation accepted it and the first use of the list failed
later. The lists now load empty, as a missing section already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…configured

A domain typed on media that lists none survived a reconfiguration,
unlike the account, the OU and the password.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… translations

Name the General page protection by its labels (password protection,
deployment password) instead of "media password", which no label uses,
and mention domain join credentials among what that password protects.
Apply the translation corrections found in a review of all cultures:
the Zero-touch mode name kept as in the navigation, standard terms for
OU, distinguished name and domain join in several languages, and a few
grammar and wording errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ease

Drop what the final design no longer reaches: ten validation texts the
authoring pages can never show, the OU validation message bindings, the
domain name carried by the OU discovery result, the test-only seed and
create paths of the runtime state files, and the split left in
FoundryConfigurationService by a loader that no longer exists.

Tighten three checks: Deploy rejects a null OU list in a hand-edited media
configuration, the runtime requires the join target to be an
organizational unit like Core and Deploy do, and the console stops
announcing a membership check that has already run.

Add tests for the cases the review found uncovered and refresh comments
that described earlier behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The latest published release (v26.10.7.1) already ships Deploy schema 15 without Domain Join settings, so the media configuration that now carries them takes the next version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NegotiatedChildAcknowledgementUsesActualLaunchIdentity starts a real powershell.exe child and expects its acknowledgement within 15 seconds. It timed out on the ARM64 CI runner while the x64 job passed, on a branch that does not touch Foundry.Bootstrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the project: foundry-bootstrap Changes in the Foundry.Bootstrap WinPE boot orchestration runtime label Oct 8, 2026
AcquireAsync_AnotherProcessLeasePreventsAcquisition starts a real powershell.exe child and expects it to hold the lease within 10 seconds. It timed out on a slow ARM64 CI runner, on a branch that does not touch the artifact cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency, package, or project metadata change project: foundry Changes in the Foundry desktop application project: foundry-bootstrap Changes in the Foundry.Bootstrap WinPE boot orchestration runtime project: foundry-core Changes in the Foundry.Core shared business logic library project: foundry-deploy Changes in the Foundry.Deploy deployment application project: foundry-postinstall Changes in the Foundry.PostInstall post-installation runtime project: shared Changes in shared localization, telemetry, or utility libraries tests Test coverage or test infrastructure change ui User interface or XAML changes

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

[Feature]: Add local Active Directory domain join support

1 participant