From cbd0b3b1c2b5808fe305e61f30747a0243b38176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Sun, 4 Oct 2026 15:10:56 +0200 Subject: [PATCH 01/84] feat(core): define domain join configuration and policy --- .../ConfigurationOverviewEvaluatorTests.cs | 32 +++ .../ConfigurationSchemaVersionsTests.cs | 2 +- .../DistinguishedNameRulesTests.cs | 58 ++++++ .../DomainJoinConfigurationTests.cs | 188 ++++++++++++++++++ .../FoundryConfigurationServiceTests.cs | 27 ++- .../ProvisioningModeSelectionTests.cs | 25 +++ .../Configuration/UnattendFileServiceTests.cs | 48 +++++ .../ConfigurationSchemaVersions.cs | 2 +- .../Deploy/DeployDomainJoinSettings.cs | 19 ++ .../FoundryDeployConfigurationDocument.cs | 3 + .../DomainJoinCredentialContext.cs | 35 ++++ .../Models/Configuration/DomainJoinMode.cs | 12 ++ .../DomainJoinOrganizationalUnitSettings.cs | 13 ++ .../Configuration/DomainJoinSettings.cs | 18 ++ .../FoundryConfigurationDocument.cs | 3 + .../PreOobe/DomainJoinActionParameters.cs | 11 + .../Models/PreOobe/DomainJoinResult.cs | 51 +++++ .../ConfigurationNavigationTarget.cs | 4 +- .../ConfigurationOverviewEvaluator.cs | 30 ++- ...onfigurationOverviewNavigationEvaluator.cs | 2 + .../Configuration/DistinguishedNameRules.cs | 151 ++++++++++++++ .../DomainJoinConfigurationValidator.cs | 126 ++++++++++++ .../FoundryConfigurationMigration.cs | 6 + .../ProvisioningModeSelectionEvaluator.cs | 28 +++ .../Configuration/UnattendFileService.cs | 21 +- .../Configuration/UnattendInspection.cs | 6 + 26 files changed, 913 insertions(+), 8 deletions(-) create mode 100644 src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs create mode 100644 src/Foundry.Core.Tests/Configuration/DomainJoinConfigurationTests.cs create mode 100644 src/Foundry.Core.Tests/Configuration/ProvisioningModeSelectionTests.cs create mode 100644 src/Foundry.Core/Models/Configuration/Deploy/DeployDomainJoinSettings.cs create mode 100644 src/Foundry.Core/Models/Configuration/DomainJoinCredentialContext.cs create mode 100644 src/Foundry.Core/Models/Configuration/DomainJoinMode.cs create mode 100644 src/Foundry.Core/Models/Configuration/DomainJoinOrganizationalUnitSettings.cs create mode 100644 src/Foundry.Core/Models/Configuration/DomainJoinSettings.cs create mode 100644 src/Foundry.Core/Models/PreOobe/DomainJoinActionParameters.cs create mode 100644 src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs create mode 100644 src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs create mode 100644 src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs create mode 100644 src/Foundry.Core/Services/Configuration/ProvisioningModeSelectionEvaluator.cs diff --git a/src/Foundry.Core.Tests/Configuration/ConfigurationOverviewEvaluatorTests.cs b/src/Foundry.Core.Tests/Configuration/ConfigurationOverviewEvaluatorTests.cs index bb0e7402e..817a390e3 100644 --- a/src/Foundry.Core.Tests/Configuration/ConfigurationOverviewEvaluatorTests.cs +++ b/src/Foundry.Core.Tests/Configuration/ConfigurationOverviewEvaluatorTests.cs @@ -9,6 +9,38 @@ namespace Foundry.Core.Tests.Configuration; public sealed class ConfigurationOverviewEvaluatorTests { + [Theory] + [InlineData(DomainJoinMode.Interactive, true, ConfigurationOverviewState.Configured)] + [InlineData(DomainJoinMode.Interactive, false, ConfigurationOverviewState.NeedsAttention)] + [InlineData(DomainJoinMode.Automatic, true, ConfigurationOverviewState.Configured)] + [InlineData(DomainJoinMode.Automatic, false, ConfigurationOverviewState.NeedsAttention)] + public void Evaluate_DomainJoinOnlyActiveModeUsesReadiness(DomainJoinMode mode, bool ready, ConfigurationOverviewState expected) + { + var configuration = new FoundryConfigurationDocument + { + DomainJoin = new DomainJoinSettings { IsEnabled = true, Mode = mode } + }; + ConfigurationOverviewEvaluation evaluation = ConfigurationOverviewEvaluator.Evaluate( + CreateContext(configuration) with { IsDomainJoinConfigurationReady = ready }); + ConfigurationOverviewItem active = mode == DomainJoinMode.Interactive ? ConfigurationOverviewItem.DomainJoinInteractive : ConfigurationOverviewItem.DomainJoinAutomatic; + ConfigurationOverviewItem inactive = mode == DomainJoinMode.Interactive ? ConfigurationOverviewItem.DomainJoinAutomatic : ConfigurationOverviewItem.DomainJoinInteractive; + Assert.Equal(expected, evaluation[active]); + Assert.Equal(ConfigurationOverviewState.NotSelected, evaluation[inactive]); + Assert.Equal(expected, ConfigurationOverviewNavigationEvaluator.EvaluateTarget(evaluation, + mode == DomainJoinMode.Interactive ? ConfigurationNavigationTarget.DomainJoinInteractive : ConfigurationNavigationTarget.DomainJoinAutomatic)); + } + + [Fact] + public void Evaluate_AutomaticDomainWithoutProtectionNeedsAttentionOnGeneralPage() + { + var configuration = new FoundryConfigurationDocument + { + DomainJoin = new DomainJoinSettings { IsEnabled = true, Mode = DomainJoinMode.Automatic } + }; + ConfigurationOverviewEvaluation evaluation = ConfigurationOverviewEvaluator.Evaluate(CreateContext(configuration)); + Assert.Equal(ConfigurationOverviewState.NeedsAttention, evaluation[ConfigurationOverviewItem.DeploymentProtection]); + } + [Theory] [InlineData(false, 0, true, ConfigurationOverviewState.Disabled)] [InlineData(true, 0, true, ConfigurationOverviewState.NeedsAttention)] diff --git a/src/Foundry.Core.Tests/Configuration/ConfigurationSchemaVersionsTests.cs b/src/Foundry.Core.Tests/Configuration/ConfigurationSchemaVersionsTests.cs index 12c298b2d..43735a3fd 100644 --- a/src/Foundry.Core.Tests/Configuration/ConfigurationSchemaVersionsTests.cs +++ b/src/Foundry.Core.Tests/Configuration/ConfigurationSchemaVersionsTests.cs @@ -11,7 +11,7 @@ public sealed class ConfigurationSchemaVersionsTests [Fact] public void CurrentSchemaVersions_MatchNextPublishedContractVersions() { - Assert.Equal(17, ConfigurationSchemaVersions.FoundryCurrent); + Assert.Equal(18, ConfigurationSchemaVersions.FoundryCurrent); Assert.Equal(15, ConfigurationSchemaVersions.DeployCurrent); Assert.Equal(5, ConfigurationSchemaVersions.ConnectCurrent); } diff --git a/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs b/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs new file mode 100644 index 000000000..5ac84057f --- /dev/null +++ b/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Services.Configuration; + +namespace Foundry.Core.Tests.Configuration; + +public sealed class DistinguishedNameRulesTests +{ + [Theory] + [InlineData("OU=Sales\\, West,DC=contoso,DC=test", "Sales, West")] + [InlineData("OU=Sales\\2C West,DC=contoso,DC=test", "Sales, West")] + [InlineData("OU=\\C3\\89quipe,DC=contoso,DC=test", "Équipe")] + [InlineData("OU=研发,DC=contoso,DC=test", "研发")] + public void ParsesEscapedAndUnicodeValuesWithoutRewritingParent(string dn, string value) + { + Assert.True(DistinguishedNameRules.TryParse(dn, out ParsedDistinguishedName parsed)); + Assert.Equal(value, parsed.Rdns[0].Attributes[0].Value); + Assert.Equal("DC=contoso,DC=test", parsed.Parent); + Assert.True(DistinguishedNameRules.IsWithinDomain(dn, "CONTOSO.TEST.")); + Assert.False(DistinguishedNameRules.IsWithinDomain(dn, "other.test")); + } + + [Theory] + [InlineData("OU=Bad\\")] + [InlineData("OU=Bad\\q,DC=contoso,DC=test")] + [InlineData("OU=Bad\\C3,DC=contoso,DC=test")] + [InlineData("OU=Bad,DC=contoso,,DC=test")] + [InlineData("OU=Bad\0Name,DC=contoso,DC=test")] + [InlineData("OU= unescaped,DC=contoso,DC=test")] + [InlineData("OU=unescaped ,DC=contoso,DC=test")] + [InlineData("OU=Bad;Name,DC=contoso,DC=test")] + public void RejectsMalformedNames(string dn) => Assert.False(DistinguishedNameRules.TryParse(dn, out _)); + + [Fact] + public void DomainSuffixCannotBeAnEscapedOrMultivaluedLookalike() + { + Assert.False(DistinguishedNameRules.IsWithinDomain("OU=PC,DC=contoso+OU=evil,DC=test", "contoso.test")); + Assert.False(DistinguishedNameRules.IsWithinDomain("OU=PC,DC=contoso\\,DC=test", "contoso.test")); + Assert.False(DistinguishedNameRules.IsWithinDomain("OU=PC,DC=child,DC=contoso,DC=test", "contoso.test")); + } + + [Theory] + [InlineData("OU=Sales=West,DC=contoso,DC=test")] + [InlineData("OU=Sales\\=West,DC=contoso,DC=test")] + public void ParsesEqualsWithinAnAttributeValue(string dn) + { + Assert.True(DistinguishedNameRules.TryParse(dn, out ParsedDistinguishedName parsed)); + Assert.Equal("Sales=West", parsed.Rdns[0].Attributes[0].Value); + } + + [Fact] + public void RejectsUnpairedUnicodeSurrogates() + { + Assert.False(DistinguishedNameRules.TryParse("OU=Bad\uD800,DC=contoso,DC=test", out _)); + } +} diff --git a/src/Foundry.Core.Tests/Configuration/DomainJoinConfigurationTests.cs b/src/Foundry.Core.Tests/Configuration/DomainJoinConfigurationTests.cs new file mode 100644 index 000000000..0b582750f --- /dev/null +++ b/src/Foundry.Core.Tests/Configuration/DomainJoinConfigurationTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; + +namespace Foundry.Core.Tests.Configuration; + +public sealed class DomainJoinConfigurationTests +{ + [Fact] + public void OmittedPasswordIsValidButAutomaticNotReady() + { + DomainJoinSettings settings = ReadyAutomatic(); + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + DomainJoinValidationResult result = DomainJoinConfigurationValidator.EvaluateReadiness(settings, false, false); + Assert.False(result.IsValid); + Assert.Contains(result.Issues, issue => issue.Code == DomainJoinValidationCode.PasswordRequired); + Assert.Contains(result.Issues, issue => issue.Code == DomainJoinValidationCode.MediaProtectionRequired); + } + + [Fact] + public void InteractiveNeedsNoMediaPassword() + { + var settings = new DomainJoinSettings { IsEnabled = true, Mode = DomainJoinMode.Interactive }; + Assert.True(DomainJoinConfigurationValidator.EvaluateReadiness(settings, false, false).IsValid); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("technician")] + [InlineData("CONTOSO\\")] + [InlineData("@contoso.test")] + [InlineData("a@b@c")] + public void IncompleteDraftAccountIsImportableButNotReady(string? account) + { + DomainJoinSettings settings = ReadyAutomatic() with { AccountName = account }; + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + Assert.False(DomainJoinConfigurationValidator.EvaluateReadiness(settings, true, true).IsValid); + } + + [Theory] + [InlineData("CONTOSO\\technician")] + [InlineData("technician@contoso.test")] + public void QualifiedAutomaticAccountIsReady(string account) + { + Assert.True(DomainJoinConfigurationValidator.EvaluateReadiness(ReadyAutomatic() with { AccountName = account }, true, true).IsValid); + } + + [Fact] + public void MissingDraftDomainIsImportableButAutomaticNotReady() + { + DomainJoinSettings settings = ReadyAutomatic() with { DomainName = null }; + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + Assert.False(DomainJoinConfigurationValidator.EvaluateReadiness(settings, true, true).IsValid); + } + + [Fact] + public void ContradictoryModesAreRejected() + { + Assert.Throws(() => DomainJoinConfigurationValidator.ThrowIfProvisioningModesConflict( + new AutopilotSettings { IsEnabled = true }, ReadyAutomatic())); + } + + [Fact] + public void CatalogAllowsDuplicateLabelsButRejectsDuplicateIdentifiersAndNames() + { + DomainJoinSettings settings = Catalog(); + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with + { + OrganizationalUnits = [settings.OrganizationalUnits[0], settings.OrganizationalUnits[1] with { Id = "one" }] + }).IsValid); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with + { + OrganizationalUnits = [settings.OrganizationalUnits[0], settings.OrganizationalUnits[1] with { DistinguishedName = "ou=Sales,dc=CONTOSO,dc=test" }] + }).IsValid); + } + + [Fact] + public void CatalogRequiresMatchingDomainAndAvailableDefault() + { + DomainJoinSettings settings = Catalog(); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with { DefaultOuId = "deleted" }).IsValid); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with { OuCatalogDomain = null }).IsValid); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with { DomainName = "other.test" }).IsValid); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with { OrganizationalUnits = [], DefaultOuId = null }).IsValid); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(settings with + { + OrganizationalUnits = [settings.OrganizationalUnits[0] with { DistinguishedName = "OU=Sales,DC=other,DC=test" }] + }).IsValid); + } + + [Fact] + public void MetadataRejectsMaximumPlusOneBoundsAndEmbeddedNull() + { + DomainJoinSettings settings = Catalog(); + foreach ((DomainJoinSettings invalid, DomainJoinValidationCode code) in new[] + { + (settings with { DomainName = new string('a', 254) }, DomainJoinValidationCode.InvalidDomainName), + (settings with { AccountName = new string('a', 513) }, DomainJoinValidationCode.InvalidAccountName), + (settings with { AccountName = "domain\\a\0b" }, DomainJoinValidationCode.InvalidAccountName), + (settings with { OrganizationalUnits = Enumerable.Range(0, 1025).Select(i => settings.OrganizationalUnits[0] with { Id = i.ToString(), DistinguishedName = $"OU=OU{i},DC=contoso,DC=test" }).ToArray() }, DomainJoinValidationCode.TooManyOrganizationalUnits), + (settings with { OrganizationalUnits = [settings.OrganizationalUnits[0] with { Id = new string('a', 129) }] }, DomainJoinValidationCode.InvalidOuId), + (settings with { OrganizationalUnits = [settings.OrganizationalUnits[0] with { DisplayName = new string('a', 121) }] }, DomainJoinValidationCode.InvalidOuDisplayName), + (settings with { OrganizationalUnits = [settings.OrganizationalUnits[0] with { DistinguishedName = "OU=" + new string('a', 4097) }] }, DomainJoinValidationCode.InvalidDistinguishedName) + }) + { + DomainJoinValidationResult result = DomainJoinConfigurationValidator.ValidateMetadata(invalid); + Assert.False(result.IsValid); + Assert.Contains(result.Issues, issue => issue.Code == code); + } + } + + [Fact] + public void MetadataAcceptsExactBoundsAndRetainsSourceDistinguishedName() + { + string id = new('i', 128); + string suffix = ",DC=contoso,DC=test"; + string dn = "OU=" + new string('a', 4096 - 3 - suffix.Length) + suffix; + DomainJoinSettings settings = Catalog() with + { + AccountName = new string('a', 512), + DefaultOuId = id, + OrganizationalUnits = [new() { Id = id, DisplayName = new string('a', 120), DistinguishedName = dn }] + }; + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + Assert.Equal(dn, settings.OrganizationalUnits[0].DistinguishedName); + settings = Catalog() with + { + DefaultOuId = "0", + OrganizationalUnits = Enumerable.Range(0, 1024).Select(i => new DomainJoinOrganizationalUnitSettings + { + Id = i.ToString(), + DisplayName = "Computers", + DistinguishedName = $"OU=OU{i},DC=contoso,DC=test" + }).ToArray() + }; + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + string maximumDomain = string.Join('.', new string('a', 63), new string('b', 63), new string('c', 63), new string('d', 61)); + Assert.True(DomainJoinConfigurationValidator.EvaluateReadiness(ReadyAutomatic() with { DomainName = maximumDomain }, true, true).IsValid); + } + + [Theory] + [InlineData("partial.")] + [InlineData("bad_domain.test")] + [InlineData("-contoso.test")] + public void IncompleteDraftDomainIsImportableButNotReady(string domain) + { + DomainJoinSettings settings = ReadyAutomatic() with { DomainName = domain }; + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid); + Assert.False(DomainJoinConfigurationValidator.EvaluateReadiness(settings, true, true).IsValid); + DomainJoinSettings catalogDraft = Catalog() with { DomainName = domain }; + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(catalogDraft).IsValid); + Assert.False(DomainJoinConfigurationValidator.EvaluateReadiness(catalogDraft, true, true).IsValid); + } + + [Fact] + public void CredentialOwnershipCanonicalizesDomainAndAccountButRejectsChangedOwners() + { + var context = new DomainJoinCredentialContext("Contoso.TEST.", "contoso\\Technician"); + Assert.True(context.Matches(new("contoso.test", "CONTOSO\\technician"))); + Assert.False(context.Matches(new("other.test", "CONTOSO\\technician"))); + Assert.False(context.Matches(new("contoso.test", "CONTOSO\\other"))); + } + + private static DomainJoinSettings ReadyAutomatic() => new() + { + IsEnabled = true, + Mode = DomainJoinMode.Automatic, + DomainName = "contoso.test", + AccountName = "CONTOSO\\technician" + }; + + private static DomainJoinSettings Catalog() => ReadyAutomatic() with + { + OuCatalogDomain = "contoso.test", + AllowOuSelectionDuringDeployment = true, + DefaultOuId = "one", + OrganizationalUnits = + [ + new() { Id = "one", DisplayName = "Computers", DistinguishedName = "OU=Sales,DC=contoso,DC=test" }, + new() { Id = "two", DisplayName = "Computers", DistinguishedName = "OU=Engineering,DC=contoso,DC=test" } + ] + }; +} diff --git a/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs b/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs index 7dd0d45c4..9ed96c4bf 100644 --- a/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs +++ b/src/Foundry.Core.Tests/Configuration/FoundryConfigurationServiceTests.cs @@ -10,6 +10,31 @@ namespace Foundry.Core.Tests.Configuration; public sealed class FoundryConfigurationServiceTests { + [Fact] + public void OldConfigurationDisablesDomainJoin() + { + FoundryConfigurationDocument document = new FoundryConfigurationService().Deserialize("{\"schemaVersion\":17}"); + Assert.Equal(18, document.SchemaVersion); + Assert.False(document.DomainJoin.IsEnabled); + } + + [Fact] + public void Deserialize_RejectsContradictoryProvisioningModesWithoutChoosingWinner() + { + Assert.Throws(() => new FoundryConfigurationService().Deserialize( + "{\"schemaVersion\":18,\"autopilot\":{\"isEnabled\":true},\"domainJoin\":{\"isEnabled\":true}}")); + } + + [Fact] + public void Deserialize_AutomaticSecretFreeDraftIsImportable() + { + FoundryConfigurationDocument document = new FoundryConfigurationService().Deserialize( + "{\"schemaVersion\":18,\"domainJoin\":{\"isEnabled\":true,\"mode\":1}}"); + Assert.True(document.DomainJoin.IsEnabled); + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(document.DomainJoin).IsValid); + Assert.False(DomainJoinConfigurationValidator.EvaluateReadiness(document.DomainJoin, false, false).IsValid); + } + [Theory] [InlineData("[\"99H1\"]", null)] [InlineData("[]", null)] @@ -39,7 +64,7 @@ public void Deserialize_PublishedConfiguration_MigratesUnavailableReleasePolicy( FoundryConfigurationDocument document = service.Deserialize(json); var generated = new DeployConfigurationGenerator().Generate(document); - Assert.Equal(17, document.SchemaVersion); + Assert.Equal(18, document.SchemaVersion); Assert.Equal(ConfigurationSchemaVersions.DeployCurrent, generated.SchemaVersion); Assert.Equal(expectedDefault is null ? [] : new[] { expectedDefault }, document.OperatingSystemSelection.AllowedReleaseIds); Assert.Equal(expectedDefault, document.OperatingSystemSelection.DefaultReleaseId); diff --git a/src/Foundry.Core.Tests/Configuration/ProvisioningModeSelectionTests.cs b/src/Foundry.Core.Tests/Configuration/ProvisioningModeSelectionTests.cs new file mode 100644 index 000000000..5832c8bcf --- /dev/null +++ b/src/Foundry.Core.Tests/Configuration/ProvisioningModeSelectionTests.cs @@ -0,0 +1,25 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Services.Configuration; + +namespace Foundry.Core.Tests.Configuration; + +public sealed class ProvisioningModeSelectionTests +{ + [Fact] + public void EveryProvisioningSwitchHasOneActiveSelection() + { + foreach (ProvisioningSelection current in Enum.GetValues()) + { + foreach (ProvisioningSelection requested in Enum.GetValues()) + { + ProvisioningSelectionDecision decision = ProvisioningModeSelectionEvaluator.Evaluate(current, requested); + Assert.Equal(current == requested ? ProvisioningSelection.None : requested, decision.Next); + Assert.Equal(current != ProvisioningSelection.None && requested != ProvisioningSelection.None && current != requested, + decision.RequiresReplacementConfirmation); + } + } + } +} diff --git a/src/Foundry.Core.Tests/Configuration/UnattendFileServiceTests.cs b/src/Foundry.Core.Tests/Configuration/UnattendFileServiceTests.cs index 655905207..3345bf967 100644 --- a/src/Foundry.Core.Tests/Configuration/UnattendFileServiceTests.cs +++ b/src/Foundry.Core.Tests/Configuration/UnattendFileServiceTests.cs @@ -11,6 +11,54 @@ namespace Foundry.Core.Tests.Configuration; public sealed class UnattendFileServiceTests { + [Fact] + public void Inspect_DomainIntentAcceptsExactlyOneApplicableConcreteNameUnchanged() + { + byte[] content = Xml(Settings("Custom-PC", "amd64") + + Settings("Other-PC", "arm64")); + byte[] original = content.ToArray(); + UnattendInspection result = UnattendFileService.Inspect(content, "x64", requiresDomainJoin: true); + Assert.Equal("Custom-PC", result.ConcreteComputerName); + Assert.False(result.HasUnattendedJoinComponent); + Assert.Equal(original, content); + } + + [Theory] + [InlineData("")] + [InlineData("*")] + [InlineData("bad name")] + [InlineData(" PC ")] + [InlineData("1234")] + [InlineData("FIRSTSECOND")] + [InlineData("Owner")] + public void Inspect_DomainIntentRejectsMissingInvalidOrConflictingNames(string settings) + { + byte[] content = Xml(Settings(settings)); + Assert.Throws(() => UnattendFileService.Inspect(content, "x64", requiresDomainJoin: true)); + UnattendFileService.Inspect(content, "x64"); + } + + [Theory] + [InlineData("")] + [InlineData("WORKGROUP")] + [InlineData("contoso.test")] + public void Inspect_DomainIntentRejectsAnyUnattendedJoinComponent(string joinSettings) + { + byte[] content = Xml(Settings("PC") + + $"{joinSettings}"); + Assert.True(UnattendFileService.Inspect(content, "x64").HasUnattendedJoinComponent); + Assert.Throws(() => UnattendFileService.Inspect(content, "x64", requiresDomainJoin: true)); + } + + [Fact] + public void Inspect_DomainIntentRejectsDuplicateSameNamesAndWrongPassName() + { + Assert.Throws(() => UnattendFileService.Inspect( + Xml(Settings("PC") + Settings("PC")), "x64", requiresDomainJoin: true)); + Assert.Throws(() => UnattendFileService.Inspect( + Xml(Settings("PC", pass: "oobeSystem")), "x64", requiresDomainJoin: true)); + } + [Theory] [InlineData("windowsPE")] [InlineData("offlineServicing")] diff --git a/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs b/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs index 8bde00a5f..9c0bb0007 100644 --- a/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs +++ b/src/Foundry.Core/Models/Configuration/ConfigurationSchemaVersions.cs @@ -6,7 +6,7 @@ namespace Foundry.Core.Models.Configuration; public static class ConfigurationSchemaVersions { - public const int FoundryCurrent = 17; + public const int FoundryCurrent = 18; public const int ConnectCurrent = 5; diff --git a/src/Foundry.Core/Models/Configuration/Deploy/DeployDomainJoinSettings.cs b/src/Foundry.Core/Models/Configuration/Deploy/DeployDomainJoinSettings.cs new file mode 100644 index 000000000..4f2ef4830 --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/Deploy/DeployDomainJoinSettings.cs @@ -0,0 +1,19 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.Configuration.Deploy; + +/// Carries active domain metadata; interactive projections omit the account and credential envelope. +public sealed record DeployDomainJoinSettings +{ + public bool IsEnabled { get; init; } + public DomainJoinMode Mode { get; init; } = DomainJoinMode.Interactive; + public string? DomainName { get; init; } + public string? AccountName { get; init; } + public string? OuCatalogDomain { get; init; } + public IReadOnlyList OrganizationalUnits { get; init; } = []; + public string? DefaultOuId { get; init; } + public bool AllowOuSelectionDuringDeployment { get; init; } + public SecretEnvelope? EncryptedCredentials { get; init; } +} diff --git a/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs b/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs index 28ac67ac0..81eeb04c3 100644 --- a/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs +++ b/src/Foundry.Core/Models/Configuration/Deploy/FoundryDeployConfigurationDocument.cs @@ -71,6 +71,9 @@ public sealed record FoundryDeployConfigurationDocument /// public DeployAutopilotSettings Autopilot { get; init; } = new(); + /// Gets domain metadata and any protected automatic credentials consumed during deployment. + public DeployDomainJoinSettings DomainJoin { get; init; } = new(); + /// /// Gets telemetry policy and runtime settings consumed by Foundry.Deploy. /// diff --git a/src/Foundry.Core/Models/Configuration/DomainJoinCredentialContext.cs b/src/Foundry.Core/Models/Configuration/DomainJoinCredentialContext.cs new file mode 100644 index 000000000..710da05c9 --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/DomainJoinCredentialContext.cs @@ -0,0 +1,35 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.Configuration; + +/// Binds a secret to its target domain and qualified account; passwords are never normalized. +public sealed record DomainJoinCredentialContext(string DomainName, string AccountName) +{ + /// Returns the canonical DNS spelling used for comparison and secret ownership. + public static string CanonicalizeDomainName(string? value) => (value ?? string.Empty).Trim().TrimEnd('.').ToLowerInvariant(); + + /// Returns the case-insensitive account spelling used for secret ownership. + public static string CanonicalizeAccountName(string? value) => (value ?? string.Empty).Trim().ToUpperInvariant(); + + /// Compares ownership without comparing or exposing a password. + public bool Matches(DomainJoinCredentialContext other) + { + ArgumentNullException.ThrowIfNull(other); + return string.Equals(CanonicalizeDomainName(DomainName), CanonicalizeDomainName(other.DomainName), StringComparison.Ordinal) && + string.Equals(CanonicalizeAccountName(AccountName), CanonicalizeAccountName(other.AccountName), StringComparison.Ordinal); + } + + /// Validates a DNS domain suitable for structural OU suffix checks. + public static bool IsValidDomainName(string? value) + { + if (string.IsNullOrWhiteSpace(value) || value.Length > 253 || value.Contains('\0')) return false; + string domain = value.Trim(); + if (domain.EndsWith('.')) domain = domain[..^1]; + string[] labels = domain.Split('.'); + return labels.Length >= 2 && labels.All(label => label.Length is >= 1 and <= 63 && + char.IsAsciiLetterOrDigit(label[0]) && char.IsAsciiLetterOrDigit(label[^1]) && + label.All(character => char.IsAsciiLetterOrDigit(character) || character == '-')); + } +} diff --git a/src/Foundry.Core/Models/Configuration/DomainJoinMode.cs b/src/Foundry.Core/Models/Configuration/DomainJoinMode.cs new file mode 100644 index 000000000..aa6321c51 --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/DomainJoinMode.cs @@ -0,0 +1,12 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.Configuration; + +/// Determines whether technicians supply credentials or media supplies protected credentials. +public enum DomainJoinMode +{ + Interactive, + Automatic +} diff --git a/src/Foundry.Core/Models/Configuration/DomainJoinOrganizationalUnitSettings.cs b/src/Foundry.Core/Models/Configuration/DomainJoinOrganizationalUnitSettings.cs new file mode 100644 index 000000000..ad84a4145 --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/DomainJoinOrganizationalUnitSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.Configuration; + +/// References an authored OU destination; live directory identity must be resolved by GUID. +public sealed record DomainJoinOrganizationalUnitSettings +{ + public string Id { get; init; } = string.Empty; + public string DisplayName { get; init; } = string.Empty; + public string DistinguishedName { get; init; } = string.Empty; +} diff --git a/src/Foundry.Core/Models/Configuration/DomainJoinSettings.cs b/src/Foundry.Core/Models/Configuration/DomainJoinSettings.cs new file mode 100644 index 000000000..88e6c9359 --- /dev/null +++ b/src/Foundry.Core/Models/Configuration/DomainJoinSettings.cs @@ -0,0 +1,18 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.Configuration; + +/// Stores portable domain metadata without persisting credentials. +public sealed record DomainJoinSettings +{ + public bool IsEnabled { get; init; } + public DomainJoinMode Mode { get; init; } = DomainJoinMode.Interactive; + public string? DomainName { get; init; } + public string? AccountName { get; init; } + public string? OuCatalogDomain { get; init; } + public IReadOnlyList OrganizationalUnits { get; init; } = []; + public string? DefaultOuId { get; init; } + public bool AllowOuSelectionDuringDeployment { get; init; } +} diff --git a/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs b/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs index 6d3e4e6e0..2a3c509ef 100644 --- a/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs +++ b/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs @@ -62,6 +62,9 @@ public sealed record FoundryConfigurationDocument /// public AutopilotSettings Autopilot { get; init; } = new(); + /// Gets optional online domain joining and its secret-free OU catalog. + public DomainJoinSettings DomainJoin { get; init; } = new(); + /// /// Gets telemetry policy and runtime settings propagated into generated media. /// diff --git a/src/Foundry.Core/Models/PreOobe/DomainJoinActionParameters.cs b/src/Foundry.Core/Models/PreOobe/DomainJoinActionParameters.cs new file mode 100644 index 000000000..7a711f74a2 --- /dev/null +++ b/src/Foundry.Core/Models/PreOobe/DomainJoinActionParameters.cs @@ -0,0 +1,11 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.PreOobe; + +/// Identifies an online join and its operation-owned sensitive payload without embedding credentials in the plan. +public sealed record DomainJoinActionParameters(string DomainName, string ComputerName, string? TargetOuDn, string CredentialPayloadPath); + +/// Verifies membership on a later boot without retaining credentials. +public sealed record DomainMembershipVerificationParameters(string DomainName, string ComputerName, string JoinActionId); diff --git a/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs b/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs new file mode 100644 index 000000000..5980e64c8 --- /dev/null +++ b/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs @@ -0,0 +1,51 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Models.PreOobe; + +/// Distinguishes observed success from failure, interrupted mutation and unverified membership. +public enum DomainJoinPhaseState { NotStarted, Succeeded, Failed, Skipped, Unknown, Unverified } + +/// Tracks restart independently from OU placement success. +public enum DomainJoinRestartState { NotRequired, Required, Requested, Completed } + +/// Tracks sensitive payload disposal independently from domain mutation outcomes. +public enum DomainJoinCleanupState { NotRequired, Pending, Disposed } + +/// Allowlisted domain failures; diagnostics never persist account names or exception text. +public enum DomainJoinFailureCode +{ + InvalidInput, CredentialUnavailable, ContextMismatch, ComputerNameMismatch, DomainUnavailable, + ReadinessTimeout, JoinFailed, PlacementFailed, MembershipMismatch, MembershipUnverified, + Interrupted, WorkerTimeout, CleanupFailed, ResultUnavailable, InvalidResult +} + +/// Reports one independent phase and an optional native or LDAP numeric diagnostic. +public sealed record DomainJoinPhaseResult +{ + public DomainJoinPhaseState State { get; init; } = DomainJoinPhaseState.NotStarted; + public DomainJoinFailureCode? FailureCode { get; init; } + public int? NativeErrorCode { get; init; } + public int? LdapErrorCode { get; init; } +} + +/// Binds password-free results to one deployment attempt, immutable plan and originating boot. +public sealed record DomainJoinResult +{ + public const int CurrentSchemaVersion = 1; + public int SchemaVersion { get; init; } = CurrentSchemaVersion; + public string OperationId { get; init; } = string.Empty; + public string AttemptId { get; init; } = string.Empty; + public string PlanHash { get; init; } = string.Empty; + public string OriginatingBootId { get; init; } = string.Empty; + public string ExpectedComputerName { get; init; } = string.Empty; + public string ExpectedDomainName { get; init; } = string.Empty; + public string? TargetOuDn { get; init; } + public DomainJoinPhaseResult Join { get; init; } = new(); + public DomainJoinPhaseResult Placement { get; init; } = new(); + public DomainJoinPhaseResult Membership { get; init; } = new(); + public DomainJoinRestartState Restart { get; init; } + public DomainJoinCleanupState Cleanup { get; init; } + public Guid? ComputerObjectGuid { get; init; } +} diff --git a/src/Foundry.Core/Services/Configuration/ConfigurationNavigationTarget.cs b/src/Foundry.Core/Services/Configuration/ConfigurationNavigationTarget.cs index 1d161316f..35b0fafd3 100644 --- a/src/Foundry.Core/Services/Configuration/ConfigurationNavigationTarget.cs +++ b/src/Foundry.Core/Services/Configuration/ConfigurationNavigationTarget.cs @@ -22,5 +22,7 @@ public enum ConfigurationNavigationTarget WindowsOptionalFeatures, AppxRemoval, AiComponentRemoval, - PostInstallation + PostInstallation, + DomainJoinInteractive, + DomainJoinAutomatic } diff --git a/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs b/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs index 700da8f16..4d11bbbbf 100644 --- a/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs +++ b/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs @@ -32,7 +32,9 @@ public enum ConfigurationOverviewItem OptionalFeatures, AppxRemoval, AiComponents, - PostInstallation + PostInstallation, + DomainJoinInteractive, + DomainJoinAutomatic } /// @@ -88,6 +90,9 @@ public sealed record ConfigurationOverviewContext /// public bool IsAutopilotConfigurationReady { get; init; } + /// Gets whether the active domain mode has valid metadata and required session inputs. + public bool IsDomainJoinConfigurationReady { get; init; } + /// /// Gets whether enabled answer-file sources, references, and media protection are ready. /// @@ -179,7 +184,8 @@ public static ConfigurationOverviewEvaluation Evaluate(ConfigurationOverviewCont [ConfigurationOverviewItem.DeploymentProtection] = EvaluateDeploymentProtection( general.DeploymentProtection.IsEnabled, context.IsDeploymentProtectionSecretReady, - OobeAccountConfigurationValidator.RequiresProtectedMedia(customization.Oobe)), + OobeAccountConfigurationValidator.RequiresProtectedMedia(customization.Oobe) || + (configuration.DomainJoin.IsEnabled && configuration.DomainJoin.Mode == DomainJoinMode.Automatic)), [ConfigurationOverviewItem.DriverOptions] = EvaluateDrivers(general, context.IsCustomDriverConfigurationReady), [ConfigurationOverviewItem.EthernetDot1x] = EvaluateNetworkTransport( configuration.Network.Dot1x.IsEnabled, @@ -206,9 +212,29 @@ public static ConfigurationOverviewEvaluation Evaluate(ConfigurationOverviewCont }; AddAutopilotStates(states, configuration.Autopilot, context.IsAutopilotConfigurationReady); + AddDomainJoinStates(states, configuration.DomainJoin, context.IsDomainJoinConfigurationReady && !configuration.Autopilot.IsEnabled); + if (configuration.Autopilot.IsEnabled && configuration.DomainJoin.IsEnabled) + { + AddAutopilotStates(states, configuration.Autopilot, false); + } return new ConfigurationOverviewEvaluation(states); } + private static void AddDomainJoinStates( + IDictionary states, + DomainJoinSettings settings, + bool isReady) + { + states[ConfigurationOverviewItem.DomainJoinInteractive] = EvaluateDomainJoinMode(DomainJoinMode.Interactive); + states[ConfigurationOverviewItem.DomainJoinAutomatic] = EvaluateDomainJoinMode(DomainJoinMode.Automatic); + + ConfigurationOverviewState EvaluateDomainJoinMode(DomainJoinMode mode) => !settings.IsEnabled || settings.Mode != mode + ? ConfigurationOverviewState.NotSelected + : isReady && DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid + ? ConfigurationOverviewState.Configured + : ConfigurationOverviewState.NeedsAttention; + } + private static bool IsDefaultCompletion(GeneralSettings settings) => settings.AutomaticRebootEnabled && DeploymentRebootDelay.NormalizeRuntime(settings.AutomaticRebootDelaySeconds) == DeploymentRebootDelay.DefaultSeconds; diff --git a/src/Foundry.Core/Services/Configuration/ConfigurationOverviewNavigationEvaluator.cs b/src/Foundry.Core/Services/Configuration/ConfigurationOverviewNavigationEvaluator.cs index 06c3258bf..48690c3ef 100644 --- a/src/Foundry.Core/Services/Configuration/ConfigurationOverviewNavigationEvaluator.cs +++ b/src/Foundry.Core/Services/Configuration/ConfigurationOverviewNavigationEvaluator.cs @@ -40,6 +40,8 @@ public static ConfigurationOverviewState EvaluateTarget( ConfigurationNavigationTarget.AutopilotJsonProfile => evaluation[ConfigurationOverviewItem.AutopilotJsonProfile], ConfigurationNavigationTarget.AutopilotHardwareHashUpload => evaluation[ConfigurationOverviewItem.AutopilotZeroTouch], ConfigurationNavigationTarget.AutopilotInteractiveHardwareHashUpload => evaluation[ConfigurationOverviewItem.AutopilotInteractive], + ConfigurationNavigationTarget.DomainJoinInteractive => evaluation[ConfigurationOverviewItem.DomainJoinInteractive], + ConfigurationNavigationTarget.DomainJoinAutomatic => evaluation[ConfigurationOverviewItem.DomainJoinAutomatic], ConfigurationNavigationTarget.OperatingSystemSelection => evaluation[ConfigurationOverviewItem.OperatingSystemSelection], ConfigurationNavigationTarget.CustomImages => evaluation[ConfigurationOverviewItem.CustomImages], ConfigurationNavigationTarget.PostInstallation => evaluation[ConfigurationOverviewItem.PostInstallation], diff --git a/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs b/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs new file mode 100644 index 000000000..896120035 --- /dev/null +++ b/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs @@ -0,0 +1,151 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Text; +using Foundry.Core.Models.Configuration; + +namespace Foundry.Core.Services.Configuration; + +/// Parses directory names structurally without rewriting their source representation. +public static class DistinguishedNameRules +{ + private static readonly UTF8Encoding StrictUtf8 = new(false, true); + + /// Accepts escaped LDAP DNs, including UTF-8 hex escapes; LDAP filter escaping is a separate operation. + public static bool TryParse(string value, out ParsedDistinguishedName parsed) + { + parsed = new([], null); + if (string.IsNullOrWhiteSpace(value) || value.Length > 4096 || value.Contains('\0')) return false; + if (!TrySplit(value, ',', out List rawRdns)) return false; + var rdns = new List(); + foreach (string rawRdn in rawRdns) + { + if (!TrySplit(rawRdn, '+', out List rawAttributes)) return false; + var attributes = new List(); + foreach (string rawAttribute in rawAttributes) + { + int equals = rawAttribute.IndexOf('='); + if (equals <= 0) return false; + string type = rawAttribute[..equals].Trim(); + if (!IsAttributeType(type) || !TryDecode(rawAttribute[(equals + 1)..], out string decoded)) return false; + attributes.Add(new(type, decoded)); + } + + rdns.Add(new(attributes)); + } + + parsed = new(rdns, rawRdns.Count > 1 ? value[(rawRdns[0].Length + 1)..] : null); + return true; + } + + /// Matches single-valued DC suffixes structurally; escaped delimiters cannot introduce a domain suffix. + public static bool IsWithinDomain(string distinguishedName, string dnsDomain) + { + if (!DomainJoinCredentialContext.IsValidDomainName(dnsDomain) || !TryParse(distinguishedName, out ParsedDistinguishedName parsed)) return false; + string[] labels = DomainJoinCredentialContext.CanonicalizeDomainName(dnsDomain).Split('.'); + if (parsed.Rdns.Count < labels.Length) return false; + int domainStart = parsed.Rdns.Count - labels.Length; + if (domainStart > 0 && parsed.Rdns[domainStart - 1].Attributes.Any(attribute => + string.Equals(attribute.Type, "DC", StringComparison.OrdinalIgnoreCase))) return false; + for (int index = 0; index < labels.Length; index++) + { + DistinguishedNameRdn rdn = parsed.Rdns[domainStart + index]; + if (rdn.Attributes.Count != 1 || !string.Equals(rdn.Attributes[0].Type, "DC", StringComparison.OrdinalIgnoreCase) || + !string.Equals(rdn.Attributes[0].Value, labels[index], StringComparison.OrdinalIgnoreCase)) return false; + } + + return true; + } + + /// Detects duplicate authored destinations only; live directory GUIDs remain authoritative. + internal static string GetComparisonKey(ParsedDistinguishedName parsed) => string.Join("|", parsed.Rdns.Select(rdn => + string.Join("+", rdn.Attributes.Select(attribute => $"{attribute.Type.ToUpperInvariant()}:{Convert.ToHexString(Encoding.UTF8.GetBytes(attribute.Value.ToUpperInvariant()))}").Order(StringComparer.Ordinal)))); + + private static bool TrySplit(string value, char separator, out List parts) + { + parts = []; + int start = 0; + for (int index = 0; index < value.Length; index++) + { + if (value[index] == '\\') + { + if (++index >= value.Length) return false; + continue; + } + + if (value[index] != separator) continue; + if (index == start) return false; + parts.Add(value[start..index]); + start = index + 1; + } + + if (start == value.Length) return false; + parts.Add(value[start..]); + return true; + } + + private static bool IsAttributeType(string type) => type.Length > 0 && + (char.IsAsciiLetter(type[0]) && type.All(character => char.IsAsciiLetterOrDigit(character) || character == '-') || + char.IsAsciiDigit(type[0]) && type.All(character => char.IsAsciiDigit(character) || character == '.') && + type.Split('.').All(part => part.Length > 0)); + + private static bool TryDecode(string raw, out string decoded) + { + decoded = string.Empty; + if (raw.Length == 0 || raw[0] is ' ' or '#' || raw[^1] == ' ' && !HasEscapedLastSpace(raw)) return false; + var builder = new StringBuilder(); + for (int index = 0; index < raw.Length; index++) + { + char character = raw[index]; + if (character != '\\') + { + if (character is '"' or '+' or ',' or ';' or '<' or '>' || char.IsControl(character)) return false; + builder.Append(character); + continue; + } + + if (++index >= raw.Length) return false; + if (index + 1 < raw.Length && Uri.IsHexDigit(raw[index]) && Uri.IsHexDigit(raw[index + 1])) + { + var bytes = new List(); + while (true) + { + bytes.Add(Convert.ToByte(raw.Substring(index, 2), 16)); + index++; + if (index + 3 >= raw.Length || raw[index + 1] != '\\' || !Uri.IsHexDigit(raw[index + 2]) || !Uri.IsHexDigit(raw[index + 3])) break; + index += 2; + } + + try { builder.Append(StrictUtf8.GetString(bytes.ToArray())); } + catch (DecoderFallbackException) { return false; } + } + else + { + if (raw[index] is not (' ' or '#' or '"' or '+' or ',' or ';' or '<' or '>' or '=' or '\\')) return false; + builder.Append(raw[index]); + } + } + + decoded = builder.ToString(); + try { StrictUtf8.GetByteCount(decoded); } + catch (EncoderFallbackException) { return false; } + return decoded.Length > 0 && !decoded.Contains('\0'); + } + + private static bool HasEscapedLastSpace(string raw) + { + int slashes = 0; + for (int index = raw.Length - 2; index >= 0 && raw[index] == '\\'; index--) slashes++; + return slashes % 2 == 1; + } +} + +/// Contains leaf-first RDNs and the original escaped parent name. +public sealed record ParsedDistinguishedName(IReadOnlyList Rdns, string? Parent); + +/// Represents one possibly multivalued relative distinguished name. +public sealed record DistinguishedNameRdn(IReadOnlyList Attributes); + +/// Contains an attribute type and decoded value for structural comparison only. +public sealed record DistinguishedNameAttribute(string Type, string Value); diff --git a/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs b/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs new file mode 100644 index 000000000..fcc5ad6ca --- /dev/null +++ b/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs @@ -0,0 +1,126 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; + +namespace Foundry.Core.Services.Configuration; + +/// Validates portable metadata separately from inputs required to build deployment media. +public static class DomainJoinConfigurationValidator +{ + public const int MaximumOrganizationalUnits = 1024; + public const int MaximumPasswordUtf8Bytes = 2560; + public const int MaximumCredentialPayloadBytes = 32 * 1024; + public const int MaximumResultBytes = 64 * 1024; + + /// Accepts incomplete secret-free drafts while rejecting unsafe bounds and inconsistent catalogs. + public static DomainJoinValidationResult ValidateMetadata(DomainJoinSettings settings) + { + ArgumentNullException.ThrowIfNull(settings); + var issues = new List(); + if (!Enum.IsDefined(settings.Mode)) Add(DomainJoinValidationCode.UnsupportedMode); + if (!IsBounded(settings.DomainName, 253)) Add(DomainJoinValidationCode.InvalidDomainName); + if (!IsBounded(settings.AccountName, 512)) Add(DomainJoinValidationCode.InvalidAccountName); + if (!IsBounded(settings.OuCatalogDomain, 253)) Add(DomainJoinValidationCode.InvalidCatalogDomain); + if (!IsBounded(settings.DefaultOuId, 128)) Add(DomainJoinValidationCode.InvalidOuId); + IReadOnlyList units = settings.OrganizationalUnits ?? []; + if (units.Count > MaximumOrganizationalUnits) Add(DomainJoinValidationCode.TooManyOrganizationalUnits); + if (units.Count == 0 && (settings.AllowOuSelectionDuringDeployment || settings.DefaultOuId is not null)) + Add(DomainJoinValidationCode.OrganizationalUnitsRequired); + var ids = new HashSet(StringComparer.OrdinalIgnoreCase); + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + if (units.Count > 0) + { + if (string.IsNullOrWhiteSpace(settings.OuCatalogDomain)) Add(DomainJoinValidationCode.CatalogDomainRequired); + else if (!DomainJoinCredentialContext.IsValidDomainName(settings.OuCatalogDomain)) Add(DomainJoinValidationCode.InvalidCatalogDomain); + if (DomainJoinCredentialContext.IsValidDomainName(settings.DomainName) && !string.Equals( + DomainJoinCredentialContext.CanonicalizeDomainName(settings.DomainName), + DomainJoinCredentialContext.CanonicalizeDomainName(settings.OuCatalogDomain), StringComparison.Ordinal)) + Add(DomainJoinValidationCode.CatalogDomainMismatch); + } + + foreach (DomainJoinOrganizationalUnitSettings unit in units.Take(MaximumOrganizationalUnits)) + { + if (unit is null) { Add(DomainJoinValidationCode.InvalidOuId); continue; } + string? issueId = IsRequiredBounded(unit.Id, 128) ? unit.Id : null; + if (issueId is null) Add(DomainJoinValidationCode.InvalidOuId); + else if (!ids.Add(unit.Id)) Add(DomainJoinValidationCode.DuplicateOuId, issueId); + if (!IsRequiredBounded(unit.DisplayName, 120)) Add(DomainJoinValidationCode.InvalidOuDisplayName, issueId); + if (!DistinguishedNameRules.TryParse(unit.DistinguishedName, out ParsedDistinguishedName parsed)) + Add(DomainJoinValidationCode.InvalidDistinguishedName, issueId); + else + { + if (!names.Add(DistinguishedNameRules.GetComparisonKey(parsed))) Add(DomainJoinValidationCode.DuplicateDistinguishedName, issueId); + if (!DistinguishedNameRules.IsWithinDomain(unit.DistinguishedName, settings.OuCatalogDomain ?? string.Empty)) + Add(DomainJoinValidationCode.OuOutsideDomain, issueId); + } + } + + if (settings.DefaultOuId is not null && !ids.Contains(settings.DefaultOuId)) Add(DomainJoinValidationCode.DefaultOuMissing); + return new(issues.Count == 0, issues); + + void Add(DomainJoinValidationCode code, string? id = null) => issues.Add(new(code, id)); + } + + /// Requires automatic credential inputs only when that mode is active; interactive media needs no password. + public static DomainJoinValidationResult EvaluateReadiness(DomainJoinSettings settings, bool hasMatchingPassword, bool isMediaProtected) + { + DomainJoinValidationResult metadata = ValidateMetadata(settings); + if (!settings.IsEnabled) return metadata; + var issues = metadata.Issues.ToList(); + if (!string.IsNullOrWhiteSpace(settings.DomainName) && !DomainJoinCredentialContext.IsValidDomainName(settings.DomainName)) + issues.Add(new(DomainJoinValidationCode.InvalidDomainName)); + if (settings.Mode == DomainJoinMode.Automatic) + { + if (string.IsNullOrWhiteSpace(settings.DomainName)) issues.Add(new(DomainJoinValidationCode.DomainNameRequired)); + if (string.IsNullOrWhiteSpace(settings.AccountName)) issues.Add(new(DomainJoinValidationCode.AccountNameRequired)); + else if (!IsQualifiedAccount(settings.AccountName)) issues.Add(new(DomainJoinValidationCode.QualifiedAccountRequired)); + if (!hasMatchingPassword) issues.Add(new(DomainJoinValidationCode.PasswordRequired)); + if (!isMediaProtected) issues.Add(new(DomainJoinValidationCode.MediaProtectionRequired)); + } + + return new(issues.Count == 0, issues); + } + + /// Rejects contradictory saved documents rather than choosing a silent import winner. + public static void ThrowIfProvisioningModesConflict(AutopilotSettings autopilot, DomainJoinSettings domainJoin) + { + ArgumentNullException.ThrowIfNull(autopilot); + ArgumentNullException.ThrowIfNull(domainJoin); + if (autopilot.IsEnabled && domainJoin.IsEnabled) + throw new InvalidOperationException("Autopilot and domain joining cannot both be enabled."); + } + + /// Accepts explicit UPN or down-level logon names supported by online joining. + public static bool IsQualifiedAccount(string? value) + { + if (!IsRequiredBounded(value, 512) || value!.Any(char.IsControl)) return false; + string account = value!.Trim(); + if (account.Any(char.IsWhiteSpace)) return false; + string[] downLevel = account.Split('\\'); + if (downLevel.Length == 2) + return downLevel.All(part => part.Length > 0 && !part.Contains('@') && !part.Contains('/')); + if (downLevel.Length != 1) return false; + string[] upn = account.Split('@'); + return upn.Length == 2 && upn[0].Length > 0 && !upn[0].Contains('/') && DomainJoinCredentialContext.IsValidDomainName(upn[1]); + } + + private static bool IsBounded(string? value, int maximum) => value is null || (value.Length <= maximum && !value.Contains('\0')); + private static bool IsRequiredBounded(string? value, int maximum) => !string.IsNullOrWhiteSpace(value) && IsBounded(value, maximum); +} + +/// Contains non-secret validation outcomes. +public sealed record DomainJoinValidationResult(bool IsValid, IReadOnlyList Issues); + +/// Identifies the rule and optional authored OU that need attention without echoing input. +public sealed record DomainJoinValidationIssue(DomainJoinValidationCode Code, string? OuId = null); + +/// Stable validation codes for authoring and runtime presentation. +public enum DomainJoinValidationCode +{ + UnsupportedMode, InvalidDomainName, InvalidAccountName, InvalidCatalogDomain, TooManyOrganizationalUnits, + InvalidOuId, InvalidOuDisplayName, InvalidDistinguishedName, DuplicateOuId, DuplicateDistinguishedName, + CatalogDomainRequired, CatalogDomainMismatch, OuOutsideDomain, DefaultOuMissing, OrganizationalUnitsRequired, + DomainNameRequired, AccountNameRequired, QualifiedAccountRequired, PasswordRequired, MediaProtectionRequired +} diff --git a/src/Foundry.Core/Services/Configuration/FoundryConfigurationMigration.cs b/src/Foundry.Core/Services/Configuration/FoundryConfigurationMigration.cs index 3a9aada3f..ed1e299ea 100644 --- a/src/Foundry.Core/Services/Configuration/FoundryConfigurationMigration.cs +++ b/src/Foundry.Core/Services/Configuration/FoundryConfigurationMigration.cs @@ -40,6 +40,12 @@ public static FoundryConfigurationDocument ApplySchemaMigrations(FoundryConfigur migrated = migrated with { CustomImages = migrated.CustomImages ?? new CustomImagesSettings() }; migrated = migrated with { PreOobe = migrated.PreOobe ?? new PreOobeSettings() }; + migrated = migrated with { DomainJoin = migrated.DomainJoin ?? new DomainJoinSettings() }; + DomainJoinConfigurationValidator.ThrowIfProvisioningModesConflict(migrated.Autopilot, migrated.DomainJoin); + if (!DomainJoinConfigurationValidator.ValidateMetadata(migrated.DomainJoin).IsValid) + { + throw new InvalidOperationException("Domain join metadata is invalid."); + } if (migrated.SchemaVersion < SupportedOperatingSystemReleasesSchemaVersion) { diff --git a/src/Foundry.Core/Services/Configuration/ProvisioningModeSelectionEvaluator.cs b/src/Foundry.Core/Services/Configuration/ProvisioningModeSelectionEvaluator.cs new file mode 100644 index 000000000..be758d084 --- /dev/null +++ b/src/Foundry.Core/Services/Configuration/ProvisioningModeSelectionEvaluator.cs @@ -0,0 +1,28 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +namespace Foundry.Core.Services.Configuration; + +/// Models a single mutually exclusive provisioning choice. +public enum ProvisioningSelection +{ + None, AutopilotJsonProfile, AutopilotHardwareHashUpload, AutopilotInteractiveHardwareHashUpload, + DomainJoinInteractive, DomainJoinAutomatic +} + +/// Describes the proposed choice; callers confirm replacement before applying it. +public sealed record ProvisioningSelectionDecision(ProvisioningSelection Next, bool RequiresReplacementConfirmation); + +/// Evaluates technician selection without mutating saved settings. +public static class ProvisioningModeSelectionEvaluator +{ + /// Toggling the active choice disables it; replacing an active choice requires confirmation. + public static ProvisioningSelectionDecision Evaluate(ProvisioningSelection current, ProvisioningSelection requested) + { + if (!Enum.IsDefined(current)) throw new ArgumentOutOfRangeException(nameof(current)); + if (!Enum.IsDefined(requested)) throw new ArgumentOutOfRangeException(nameof(requested)); + return new(current == requested ? ProvisioningSelection.None : requested, + current != ProvisioningSelection.None && requested != ProvisioningSelection.None && current != requested); + } +} diff --git a/src/Foundry.Core/Services/Configuration/UnattendFileService.cs b/src/Foundry.Core/Services/Configuration/UnattendFileService.cs index 708b9236d..1b88407c0 100644 --- a/src/Foundry.Core/Services/Configuration/UnattendFileService.cs +++ b/src/Foundry.Core/Services/Configuration/UnattendFileService.cs @@ -88,7 +88,7 @@ public static byte[] ReadValidated(UnattendFileSettings file) /// Checks supported passes and applicable component settings without certifying Windows SIM compatibility. /// Auxiliary architectures remain unchanged; only applicable components contribute conflict signals. /// - public static UnattendInspection Inspect(byte[] content, string? targetArchitecture = null) + public static UnattendInspection Inspect(byte[] content, string? targetArchitecture = null, bool requiresDomainJoin = false) { XDocument document = Parse(content); XElement root = document.Root!; @@ -107,6 +107,8 @@ public static UnattendInspection Inspect(byte[] content, string? targetArchitect bool hasApplicableSettings = false; bool hasCommands = false; bool conflictsWithAutopilot = false; + bool hasUnattendedJoinComponent = false; + var computerNames = new List(); foreach (XElement settings in root.Elements(UnattendNamespace + "settings")) { string? pass = (string?)settings.Attribute("pass"); @@ -123,6 +125,7 @@ public static UnattendInspection Inspect(byte[] content, string? targetArchitect foreach (XElement component in settings.Elements(UnattendNamespace + "component")) { + hasUnattendedJoinComponent |= (string?)component.Attribute("name") == "Microsoft-Windows-UnattendedJoin"; if (string.IsNullOrWhiteSpace((string?)component.Attribute("name")) || !component.Elements().Any(element => element.Name.Namespace == UnattendNamespace)) { continue; @@ -144,6 +147,11 @@ public static UnattendInspection Inspect(byte[] content, string? targetArchitect hasApplicableSettings = true; + if (name == "Microsoft-Windows-Shell-Setup" && pass == "specialize") + { + computerNames.AddRange(Children(component, "ComputerName").Select(element => element.Value)); + } + hasCommands |= HasCommands(component, name, pass); conflictsWithAutopilot |= ConflictsWithAutopilot(component, name, pass); } @@ -154,11 +162,20 @@ public static UnattendInspection Inspect(byte[] content, string? targetArchitect throw new InvalidDataException("The answer file has no supported component settings applicable to the selected architecture."); } + string? concreteComputerName = computerNames.Count == 1 && ComputerNameRules.IsValid(computerNames[0]) + ? computerNames[0] : null; + if (requiresDomainJoin && (hasUnattendedJoinComponent || concreteComputerName is null)) + { + throw new InvalidDataException("Domain joining requires exactly one valid specialize computer name and no UnattendedJoin component."); + } + return new UnattendInspection { Architectures = architectures.Order(StringComparer.Ordinal).ToArray(), HasCommands = hasCommands, - ConflictsWithAutopilot = conflictsWithAutopilot + ConflictsWithAutopilot = conflictsWithAutopilot, + HasUnattendedJoinComponent = hasUnattendedJoinComponent, + ConcreteComputerName = concreteComputerName }; } diff --git a/src/Foundry.Core/Services/Configuration/UnattendInspection.cs b/src/Foundry.Core/Services/Configuration/UnattendInspection.cs index 691dc954c..d72eebca9 100644 --- a/src/Foundry.Core/Services/Configuration/UnattendInspection.cs +++ b/src/Foundry.Core/Services/Configuration/UnattendInspection.cs @@ -23,4 +23,10 @@ public sealed record UnattendInspection /// Gets whether applicable known settings take ownership of enrollment-sensitive OOBE or accounts. /// public bool ConflictsWithAutopilot { get; init; } + + /// Gets whether a join component exists, including empty and workgroup-only components. + public bool HasUnattendedJoinComponent { get; init; } + + /// Gets exactly one valid applicable specialize name, preserving its original spelling. + public string? ConcreteComputerName { get; init; } } From c4486eae5e5802fca0ce9fc3c1b54ae81297be91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Sun, 4 Oct 2026 15:25:43 +0200 Subject: [PATCH 02/84] feat(profiles): protect domain join credentials and media projection --- .../DeployConfigurationGeneratorTests.cs | 49 +++++++ .../DomainJoinSecretStateTests.cs | 111 ++++++++++++++ .../Profiles/DeploymentBuildSnapshotTests.cs | 27 ++++ .../Profiles/DeploymentProfileMergeTests.cs | 34 +++++ .../DeploymentProfilePackageServiceTests.cs | 51 ++++++- .../DeploymentProfileProjectionTests.cs | 10 ++ .../LocalDeploymentProfileRepositoryTests.cs | 32 ++++ .../Profiles/DeploymentProfileDocument.cs | 2 +- .../DeployConfigurationGenerator.cs | 51 +++++++ .../DomainJoinCredentialPayloadCodec.cs | 138 ++++++++++++++++++ .../Configuration/DomainJoinSecretState.cs | 70 +++++++++ .../IDeployConfigurationGenerator.cs | 9 ++ .../Profiles/DeploymentBuildSnapshot.cs | 21 ++- .../Profiles/DeploymentProfileMerge.cs | 21 ++- .../DeploymentProfilePackageService.cs | 12 +- .../Profiles/DeploymentProfilePayload.cs | 4 +- .../DeploymentProfileSecretBinding.cs | 2 + .../ServiceCollectionExtensions.cs | 1 + .../DeploymentProfileCoordinator.cs | 11 +- .../DeploymentProfileSessionService.cs | 77 ++++++++-- .../DomainJoinSecretStateService.cs | 31 ++++ .../FoundryConfigurationStateService.cs | 32 +++- .../IDomainJoinSecretStateService.cs | 19 +++ 23 files changed, 791 insertions(+), 24 deletions(-) create mode 100644 src/Foundry.Core.Tests/Configuration/DomainJoinSecretStateTests.cs create mode 100644 src/Foundry.Core/Services/Configuration/DomainJoinCredentialPayloadCodec.cs create mode 100644 src/Foundry.Core/Services/Configuration/DomainJoinSecretState.cs create mode 100644 src/Foundry/Services/Configuration/DomainJoinSecretStateService.cs create mode 100644 src/Foundry/Services/Configuration/IDomainJoinSecretStateService.cs diff --git a/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs b/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs index 51fa56b8a..f8c88da48 100644 --- a/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs +++ b/src/Foundry.Core.Tests/Configuration/DeployConfigurationGeneratorTests.cs @@ -14,6 +14,55 @@ namespace Foundry.Core.Tests.Configuration; public sealed class DeployConfigurationGeneratorTests { + [Theory] + [InlineData(false, DomainJoinMode.Automatic)] + [InlineData(true, DomainJoinMode.Interactive)] + public void ManualAndDisabledMediaExcludeAutomaticCredentials(bool enabled, DomainJoinMode mode) + { + using var secrets = new DomainJoinSecretState(); + secrets.SetPassword(new("example.com", "EXAMPLE\\joiner"), "password"); + var document = new FoundryConfigurationDocument { DomainJoin = new() { IsEnabled = enabled, Mode = mode, DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } }; + var media = new DeployConfigurationGenerator().Generate(document, null, null, null, secrets); + Assert.Equal(enabled, media.DomainJoin.IsEnabled); + Assert.Null(media.DomainJoin.AccountName); + Assert.Null(media.DomainJoin.EncryptedCredentials); + } + + [Theory] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public void AutomaticRequiresExistingProtection(bool protectedMedia, bool hasKey, bool hasPassword) + { + using var secrets = new DomainJoinSecretState(); + if (hasPassword) secrets.SetPassword(new("example.com", "EXAMPLE\\joiner"), "password"); + var document = new FoundryConfigurationDocument { DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } }; + Assert.Throws(() => new DeployConfigurationGenerator().Generate(document, hasKey ? new byte[32] : null, + new() { IsEnabled = protectedMedia }, null, secrets)); + } + + [Fact] + public void EnvelopeAuthenticatesCredentialContext() + { + using var secrets = new DomainJoinSecretState(); + var context = new DomainJoinCredentialContext("example.com", "EXAMPLE\\joiner"); + secrets.SetPassword(context, " exact password "); + byte[] key = new byte[32]; + var document = new FoundryConfigurationDocument { DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = context.DomainName, AccountName = context.AccountName } }; + var media = new DeployConfigurationGenerator().Generate(document, key, new() { IsEnabled = true }, null, secrets); + Assert.NotNull(media.DomainJoin.EncryptedCredentials); + byte[] plaintext = MediaSecretEnvelopeProtector.DecryptBytes(media.DomainJoin.EncryptedCredentials, key, MediaSecretEnvelopeProtector.DeploymentKeyId); + try + { + using var decoded = DomainJoinCredentialPayloadCodec.Decode(plaintext, context); + Assert.Equal(" exact password ", new string(decoded.Password.Span)); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(plaintext, new("other.com", context.AccountName))); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(plaintext, new(context.DomainName, "EXAMPLE\\other"))); + Assert.DoesNotContain("exact password", new DeployConfigurationGenerator().Serialize(media)); + } + finally { System.Security.Cryptography.CryptographicOperations.ZeroMemory(plaintext); } + } + [Theory] [InlineData("26.10.3.1")] [InlineData(null)] diff --git a/src/Foundry.Core.Tests/Configuration/DomainJoinSecretStateTests.cs b/src/Foundry.Core.Tests/Configuration/DomainJoinSecretStateTests.cs new file mode 100644 index 000000000..65f943bbe --- /dev/null +++ b/src/Foundry.Core.Tests/Configuration/DomainJoinSecretStateTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; + +namespace Foundry.Core.Tests.Configuration; + +public sealed class DomainJoinSecretStateTests +{ + private static readonly DomainJoinCredentialContext Context = new("example.com", "EXAMPLE\\joiner"); + + [Fact] + public void CodecPreservesPasswordAndClearsDecodedBuffer() + { + byte[] encoded = DomainJoinCredentialPayloadCodec.Encode(Context, " exact password "); + using DomainJoinCredentialPayload decoded = DomainJoinCredentialPayloadCodec.Decode(encoded, new("EXAMPLE.COM.", "example\\JOINER")); + ReadOnlyMemory owned = decoded.Password; + Assert.Equal(" exact password ", new string(owned.Span)); + decoded.Dispose(); + Assert.All(owned.ToArray(), value => Assert.Equal('\0', value)); + } + + [Fact] + public void CodecRejectsContextSubstitutionAndMalformedPayload() + { + byte[] encoded = DomainJoinCredentialPayloadCodec.Encode(Context, "password"); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(encoded, new("other.com", Context.AccountName))); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(encoded, new("example.com", "EXAMPLE\\other"))); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(encoded.AsSpan(0, encoded.Length - 1))); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(new byte[32769])); + } + + [Theory] + [InlineData("\0")] + public void CodecRejectsUnsafePassword(string password) + { + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Encode(Context, password)); + } + + [Fact] + public void CodecRejectsUnpairedSurrogate() + { + string password = new('\ud800', 1); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Encode(Context, password)); + } + + [Fact] + public void CodecRejectsTrailingBytesAndInvalidUtf8() + { + byte[] encoded = DomainJoinCredentialPayloadCodec.Encode(Context, "password"); + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode([.. encoded, 0])); + encoded[^1] = 0xff; + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Decode(encoded)); + } + + [Fact] + public void CodecEnforcesPasswordUtf8Bound() + { + Assert.Throws(() => DomainJoinCredentialPayloadCodec.Encode(Context, new string('é', 1281))); + using var state = new DomainJoinSecretState(); + Assert.Throws(() => state.SetPassword(Context, new string('a', 2561))); + } + + [Fact] + public void ContextChangeClearsPassword() + { + using var state = new DomainJoinSecretState(); + state.SetPassword(Context, " original password "); + Assert.True(state.HasPassword(new(" EXAMPLE.COM. ", "example\\JOINER"))); + Assert.Equal(" original password ", new string(state.GetPasswordCopy(Context)!)); + Assert.True(state.Update(new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = "other.example.com", AccountName = Context.AccountName })); + Assert.False(state.HasPassword(Context)); + Assert.Null(state.GetPasswordCopy(Context)); + } + + [Theory] + [InlineData(false, DomainJoinMode.Automatic)] + [InlineData(true, DomainJoinMode.Interactive)] + public void LeavingAutomaticModeClearsPassword(bool enabled, DomainJoinMode mode) + { + using var state = new DomainJoinSecretState(); + state.SetPassword(Context, "password"); + Assert.True(state.Update(new() { IsEnabled = enabled, Mode = mode, DomainName = Context.DomainName, AccountName = Context.AccountName })); + Assert.False(state.HasPassword(Context)); + } + + [Fact] + public void OwnedCopiesAreZeroedOnDispose() + { + var state = new DomainJoinSecretState(); + char[] input = "password".ToCharArray(); + state.SetPassword(Context, input); + char[] owned = (char[])typeof(DomainJoinSecretState).GetField("password", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(state)!; + state.Dispose(); + Assert.All(owned, value => Assert.Equal('\0', value)); + Assert.Equal("password", new string(input)); + Assert.Throws(() => state.GetPasswordCopy(Context)); + } + + [Fact] + public void DifferentContextCannotReadPassword() + { + using var state = new DomainJoinSecretState(); + state.SetPassword(Context, "password"); + Assert.False(state.HasPassword(new("example.com", "EXAMPLE\\other"))); + Assert.Null(state.GetPasswordCopy(new("other.com", Context.AccountName))); + } +} diff --git a/src/Foundry.Core.Tests/Profiles/DeploymentBuildSnapshotTests.cs b/src/Foundry.Core.Tests/Profiles/DeploymentBuildSnapshotTests.cs index 557d7cdf6..cb07ee8e8 100644 --- a/src/Foundry.Core.Tests/Profiles/DeploymentBuildSnapshotTests.cs +++ b/src/Foundry.Core.Tests/Profiles/DeploymentBuildSnapshotTests.cs @@ -17,6 +17,33 @@ public sealed class DeploymentBuildSnapshotTests : IDisposable { private readonly string root = Path.Combine(Path.GetTempPath(), "FoundrySnapshotTests", Guid.NewGuid().ToString("N")); + [Fact] + public async Task FrozenSecretSurvivesAuthoringChange() + { + using var accounts = new OobeAccountSecretState(); + using var domain = new DomainJoinSecretState(); + var context = new DomainJoinCredentialContext("example.com", "EXAMPLE\\joiner"); + domain.SetPassword(context, " frozen password "); + var document = new FoundryConfigurationDocument + { + General = new() { DeploymentProtection = new() { IsEnabled = true } }, + DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = context.DomainName, AccountName = context.AccountName } + }; + Task capturing = DeploymentBuildSnapshot.CaptureAsync(document, accounts, domain, "MediaPassword123!", root, null, TestContext.Current.CancellationToken); + domain.Clear(); + using DeploymentBuildSnapshot snapshot = await capturing; + using var protection = snapshot.CreateDeploymentProtectionMaterial(); + string json = snapshot.GenerateDeployConfigurationJson(deploymentSecretsKey: protection.DeploymentKey, protectionSettings: protection.Settings); + var deploy = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + byte[] payload = MediaSecretEnvelopeProtector.DecryptBytes(deploy.DomainJoin.EncryptedCredentials!, protection.DeploymentKey, MediaSecretEnvelopeProtector.DeploymentKeyId); + try + { + using var decoded = DomainJoinCredentialPayloadCodec.Decode(payload, context); + Assert.Equal(" frozen password ", new string(decoded.Password.Span)); + } + finally { CryptographicOperations.ZeroMemory(payload); } + } + [Fact] public async Task CaptureAsync_PreservesOneAuthoringVersionAcrossBothRuntimeOutputs() { diff --git a/src/Foundry.Core.Tests/Profiles/DeploymentProfileMergeTests.cs b/src/Foundry.Core.Tests/Profiles/DeploymentProfileMergeTests.cs index b6bc8d254..d72002791 100644 --- a/src/Foundry.Core.Tests/Profiles/DeploymentProfileMergeTests.cs +++ b/src/Foundry.Core.Tests/Profiles/DeploymentProfileMergeTests.cs @@ -10,6 +10,40 @@ namespace Foundry.Core.Tests.Profiles; public sealed class DeploymentProfileMergeTests { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DomainOmittedMetadataSurvivesRecaptureWithoutRevivingPassword(bool includeSecrets) + { + DeploymentProfileDocument baseline = new() { ProfileId = Guid.NewGuid(), Configuration = new() { DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } } }; + string identity = DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, baseline); + baseline = baseline with { Secrets = new() { Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = identity, State = ProfileValueState.Omitted }] } }; + var captured = baseline with { Secrets = new() { Entries = [baseline.Secrets.Entries[0] with { State = ProfileValueState.Unavailable }] } }; + var result = DeploymentProfileMerge.PreserveOmittedSourceMetadata(captured, baseline, baseline.Configuration, includeSecrets); + Assert.Equal(ProfileValueState.Omitted, Assert.Single(result.Secrets.Entries).State); + Assert.Null(result.Secrets.Entries[0].Value); + captured = captured with { Secrets = new(), Configuration = captured.Configuration with { DomainJoin = captured.Configuration.DomainJoin with { AccountName = "EXAMPLE\\other" } } }; + Assert.Empty(DeploymentProfileMerge.PreserveOmittedSourceMetadata(captured, baseline, baseline.Configuration, includeSecrets).Secrets.Entries); + } + + [Fact] + public void DomainOmissionRetainsOnlyMatchingContext() + { + DeploymentProfileDocument local = new() { ProfileId = Guid.NewGuid(), Configuration = new() { DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } } }; + string identity = DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, local); + local = local with { Secrets = new() { Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = identity, State = ProfileValueState.Present, Value = [112, 97, 115, 115] }] } }; + DeploymentProfileDocument omitted = local with { Secrets = new() { Entries = [local.Secrets.Entries[0] with { State = ProfileValueState.Omitted, Value = null }] } }; + var retained = DeploymentProfileMerge.PreserveOmittedLocalValues(omitted, local); + Assert.Equal(new byte[] { 112, 97, 115, 115 }, retained.Secrets.Entries[0].Value); + Assert.NotSame(local.Secrets.Entries[0].Value, retained.Secrets.Entries[0].Value); + var disabled = omitted with { Configuration = omitted.Configuration with { DomainJoin = omitted.Configuration.DomainJoin with { IsEnabled = false } } }; + Assert.Null(DeploymentProfileMerge.PreserveOmittedLocalValues(disabled, local).Secrets.Entries[0].Value); + omitted = omitted with { Configuration = omitted.Configuration with { DomainJoin = omitted.Configuration.DomainJoin with { AccountName = "EXAMPLE\\other" } } }; + // Even a stale imported identity must not carry a password to another account. + var changed = DeploymentProfileMerge.PreserveOmittedLocalValues(omitted, local); + Assert.Null(changed.Secrets.Entries[0].Value); + } + [Fact] public void Merge_SettingsOnlyRoundTripRetainsAnotherPcsCertificate() { diff --git a/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs b/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs index 704850996..a8f5e9384 100644 --- a/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs +++ b/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs @@ -15,6 +15,55 @@ public sealed class DeploymentProfilePackageServiceTests { private readonly DeploymentProfilePackageService _service = new(); + [Fact] + public void SharedRevisionRejectsEmbeddedNulDomainPassword() + { + var profile = CreateProfile() with { Secrets = new() { Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = "domain", State = ProfileValueState.Present, Value = [112, 0, 119] }] } }; + Assert.Throws(() => _service.Encrypt(profile, new byte[32], ProfilePackagePurpose.SharedRevision)); + } + + [Fact] + public void ExportOmitsDomainPasswordWithoutMutatingSource() + { + byte[] password = Encoding.UTF8.GetBytes("domain-password"); + DeploymentProfileDocument profile = CreateProfile() with + { + Configuration = new() { DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } }, + Secrets = new() + { + Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = "domain", State = ProfileValueState.Present, Value = password }, + new() { Purpose = ProfileSecretPurpose.WifiPassphrase, Identity = "wifi", State = ProfileValueState.Present, Value = Encoding.UTF8.GetBytes("wifi-password") }] + } + }; + DeploymentProfileDocument imported = _service.Import(_service.Export(profile, "password"), "password"); + Assert.Equal(ProfileValueState.Omitted, imported.Secrets.Entries[0].State); + Assert.Null(imported.Secrets.Entries[0].Value); + Assert.Equal("wifi-password", Encoding.UTF8.GetString(imported.Secrets.Entries[1].Value!)); + Assert.Equal("domain-password", Encoding.UTF8.GetString(password)); + Assert.Equal(ProfileValueState.Present, profile.Secrets.Entries[0].State); + Assert.Equal("example.com", imported.Configuration.DomainJoin.DomainName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SharedEncryptedRevisionHonorsIncludeSecrets(bool includeSecrets) + { + DeploymentProfileDocument profile = CreateProfile() with + { + Secrets = new() + { + Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = "domain", + State = includeSecrets ? ProfileValueState.Present : ProfileValueState.Omitted, + Value = includeSecrets ? Encoding.UTF8.GetBytes("domain-password") : null }] + } + }; + byte[] key = new byte[32]; + DeploymentProfileDocument revision = _service.Decrypt(_service.Encrypt(profile, key, ProfilePackagePurpose.SharedRevision), key, ProfilePackagePurpose.SharedRevision); + Assert.Equal(includeSecrets ? ProfileValueState.Present : ProfileValueState.Omitted, Assert.Single(revision.Secrets.Entries).State); + Assert.Equal(includeSecrets ? "domain-password" : null, revision.Secrets.Entries[0].Value is { } value ? Encoding.UTF8.GetString(value) : null); + } + [Fact] public void ExportImport_PreservesSelectedSecretsAndExactAssetBytes() { @@ -383,7 +432,7 @@ public void Export_RejectsUnsafeInlineAutopilotFolder(string folder) [Fact] public void ExportImport_PreservesSafeUnicodeAutopilotFolderAndInlineJson() { - AutopilotProfileSettings autopilot = CreateAutopilotProfile() with { FolderName = "Équipe_Paris" }; + AutopilotProfileSettings autopilot = CreateAutopilotProfile() with { FolderName = "Équipe_Paris" }; DeploymentProfileDocument profile = CreateProfile() with { Configuration = new() { Autopilot = new() { Profiles = [autopilot] } } }; AutopilotProfileSettings imported = Assert.Single(_service.Import(_service.Export(profile, "password"), "password").Configuration.Autopilot.Profiles); Assert.Equal(autopilot.FolderName, imported.FolderName); diff --git a/src/Foundry.Core.Tests/Profiles/DeploymentProfileProjectionTests.cs b/src/Foundry.Core.Tests/Profiles/DeploymentProfileProjectionTests.cs index 8f71e7be9..a0d9d5da1 100644 --- a/src/Foundry.Core.Tests/Profiles/DeploymentProfileProjectionTests.cs +++ b/src/Foundry.Core.Tests/Profiles/DeploymentProfileProjectionTests.cs @@ -10,6 +10,16 @@ namespace Foundry.Core.Tests.Profiles; public sealed class DeploymentProfileProjectionTests { + [Fact] + public void DomainSecretIdentityUsesCanonicalOwnership() + { + var first = new DeploymentProfileDocument { Configuration = new() { DomainJoin = new() { DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } } }; + var second = first with { Configuration = first.Configuration with { DomainJoin = first.Configuration.DomainJoin with { DomainName = " EXAMPLE.COM. ", AccountName = "example\\JOINER" } } }; + Assert.Equal(DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, first), DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, second)); + second = second with { Configuration = second.Configuration with { DomainJoin = second.Configuration.DomainJoin with { AccountName = "EXAMPLE\\other" } } }; + Assert.NotEqual(DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, first), DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, second)); + } + [Fact] public void PortableContent_IgnoresHostPathsTelemetryAndRecordOrdering() { diff --git a/src/Foundry.Core.Tests/Profiles/LocalDeploymentProfileRepositoryTests.cs b/src/Foundry.Core.Tests/Profiles/LocalDeploymentProfileRepositoryTests.cs index e83a43d21..63995968f 100644 --- a/src/Foundry.Core.Tests/Profiles/LocalDeploymentProfileRepositoryTests.cs +++ b/src/Foundry.Core.Tests/Profiles/LocalDeploymentProfileRepositoryTests.cs @@ -10,6 +10,7 @@ using Foundry.Core.Models.Configuration; using Foundry.Core.Models.Profiles; using Foundry.Core.Services.Packages; +using Foundry.Core.Services.Configuration; using Foundry.Core.Services.Profiles; using Foundry.Utilities.Security; @@ -21,6 +22,37 @@ public sealed class LocalDeploymentProfileRepositoryTests : IDisposable private readonly FakeCredentials credentials = new(); private readonly Guid localId = Guid.NewGuid(); + [Fact] + public void ExportedDomainDraftCanBeRememberedButIsNotReadyForAutomaticMedia() + { + var packages = new DeploymentProfilePackageService(); + var profile = CreateProfile() with + { + Configuration = new() { DomainJoin = new() { IsEnabled = true, Mode = DomainJoinMode.Automatic, DomainName = "example.com", AccountName = "EXAMPLE\\joiner" } }, + Secrets = new() { Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = "domain", State = ProfileValueState.Present, Value = Encoding.UTF8.GetBytes("domain-password") }] } + }; + var imported = packages.Import(packages.Export(profile, "password"), "password"); + var repository = CreateRepository(); + repository.Save(localId, imported, true, null); + using LocalProfileSnapshot read = repository.Read(localId); + Assert.Equal(ProfileValueState.Omitted, Assert.Single(read.Profile.Secrets.Entries).State); + Assert.Throws(() => new DeployConfigurationGenerator().Generate(read.Profile.Configuration, new byte[32], new() { IsEnabled = true }, null)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DomainPasswordRetentionHonorsRememberSecrets(bool remember) + { + var repository = CreateRepository(); + var profile = CreateProfile() with { Secrets = new() { Entries = [new() { Purpose = ProfileSecretPurpose.DomainJoinPassword, Identity = "domain", State = ProfileValueState.Present, Value = Encoding.UTF8.GetBytes("domain-password") }] } }; + repository.Save(localId, profile, remember, null); + using LocalProfileSnapshot read = repository.Read(localId); + var secret = Assert.Single(read.Profile.Secrets.Entries); + Assert.Equal(remember ? ProfileValueState.Present : ProfileValueState.Omitted, secret.State); + Assert.Equal(remember ? "domain-password" : null, secret.Value is { } value ? Encoding.UTF8.GetString(value) : null); + } + [Fact] public async Task ListAsync_WhenContentionEnds_ReturnsCommittedProfiles() { diff --git a/src/Foundry.Core/Models/Profiles/DeploymentProfileDocument.cs b/src/Foundry.Core/Models/Profiles/DeploymentProfileDocument.cs index f5382cc0f..0e9e5ae65 100644 --- a/src/Foundry.Core/Models/Profiles/DeploymentProfileDocument.cs +++ b/src/Foundry.Core/Models/Profiles/DeploymentProfileDocument.cs @@ -53,7 +53,7 @@ public sealed record DeploymentProfileAsset public enum ProfileValueState { Omitted, Unavailable, Deleted, Blank, Present } /// Allowlisted password families, independent of operating-system credential storage names. -public enum ProfileSecretPurpose { DeploymentPassword, WifiPassphrase, AdministratorPassword, AdditionalAccountPassword, WiredCertificatePassword, WifiCertificatePassword, AutopilotCertificatePassword, SharedProfileKey } +public enum ProfileSecretPurpose { DeploymentPassword, WifiPassphrase, AdministratorPassword, AdditionalAccountPassword, WiredCertificatePassword, WifiCertificatePassword, AutopilotCertificatePassword, SharedProfileKey, DomainJoinPassword } /// Allowlisted portable inputs. Opaque files are included only when the caller explicitly supplies their bytes. public enum ProfileAssetKind { Unattend, WiredProfile, WifiProfile, WiredCertificate, WifiCertificate, AutopilotProfile, AutopilotCertificate } diff --git a/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs b/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs index c31982478..80ea44398 100644 --- a/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs +++ b/src/Foundry.Core/Services/Configuration/DeployConfigurationGenerator.cs @@ -57,9 +57,25 @@ public FoundryDeployConfigurationDocument Generate( byte[]? deploymentSecretsKey, DeployProtectionSettings? protectionSettings, OobeAccountSecretState? oobeAccountSecretState, + string? authoringVersion = null) => + Generate(document, deploymentSecretsKey, protectionSettings, oobeAccountSecretState, null, authoringVersion); + + /// + public FoundryDeployConfigurationDocument Generate( + FoundryConfigurationDocument document, + byte[]? deploymentSecretsKey, + DeployProtectionSettings? protectionSettings, + OobeAccountSecretState? oobeAccountSecretState, + DomainJoinSecretState? domainJoinSecretState, string? authoringVersion = null) { ArgumentNullException.ThrowIfNull(document); + DomainJoinConfigurationValidator.ThrowIfProvisioningModesConflict(document.Autopilot, document.DomainJoin); + var domainContext = new DomainJoinCredentialContext(document.DomainJoin.DomainName ?? string.Empty, document.DomainJoin.AccountName ?? string.Empty); + if (!DomainJoinConfigurationValidator.EvaluateReadiness(document.DomainJoin, + domainJoinSecretState?.HasPassword(domainContext) == true, + protectionSettings?.IsEnabled == true && deploymentSecretsKey is { Length: MediaSecretEnvelopeProtector.KeySizeBytes }).IsValid) + throw new InvalidOperationException("Domain joining configuration is not ready for deployment media."); CustomImageSettingsValidator.ThrowIfInvalid(document.CustomImages); PreOobeConfigurationValidator.ThrowIfInvalid(document.PreOobe); UnattendFileService.ValidateSettings(document.Unattend, protectionSettings?.IsEnabled == true); @@ -72,6 +88,7 @@ public FoundryDeployConfigurationDocument Generate( return new FoundryDeployConfigurationDocument { AuthoringVersion = authoringVersion, + DomainJoin = MapDomainJoinSettings(document.DomainJoin, deploymentSecretsKey, domainJoinSecretState), Protection = protectionSettings ?? new DeployProtectionSettings(), PreOobe = new DeployPreOobeSettings { @@ -184,6 +201,40 @@ public FoundryDeployConfigurationDocument Generate( }; } + private static DeployDomainJoinSettings MapDomainJoinSettings(DomainJoinSettings settings, byte[]? key, DomainJoinSecretState? secrets) + { + if (!settings.IsEnabled) return new(); + SecretEnvelope? envelope = null; + if (settings.Mode == DomainJoinMode.Automatic) + { + var context = new DomainJoinCredentialContext(settings.DomainName!, settings.AccountName!); + char[] password = secrets!.GetPasswordCopy(context)!; + byte[]? payload = null; + try + { + payload = DomainJoinCredentialPayloadCodec.Encode(context, password); + envelope = MediaSecretEnvelopeProtector.EncryptBytes(payload, key!, MediaSecretEnvelopeProtector.DeploymentKeyId); + } + finally + { + CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); + if (payload is not null) CryptographicOperations.ZeroMemory(payload); + } + } + return new() + { + IsEnabled = true, + Mode = settings.Mode, + DomainName = settings.DomainName, + AccountName = settings.Mode == DomainJoinMode.Automatic ? settings.AccountName : null, + OuCatalogDomain = settings.OuCatalogDomain, + OrganizationalUnits = settings.OrganizationalUnits.Select(unit => unit with { }).ToArray(), + DefaultOuId = settings.DefaultOuId, + AllowOuSelectionDuringDeployment = settings.AllowOuSelectionDuringDeployment, + EncryptedCredentials = envelope + }; + } + private static PreOobeActionSettings ClonePreOobeAction(PreOobeActionSettings action) => action with { Package = action.Package is { } package ? package with { } : null, diff --git a/src/Foundry.Core/Services/Configuration/DomainJoinCredentialPayloadCodec.cs b/src/Foundry.Core/Services/Configuration/DomainJoinCredentialPayloadCodec.cs new file mode 100644 index 000000000..ac4e431f2 --- /dev/null +++ b/src/Foundry.Core/Services/Configuration/DomainJoinCredentialPayloadCodec.cs @@ -0,0 +1,138 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Foundry.Core.Models.Configuration; + +namespace Foundry.Core.Services.Configuration; + +/// Encodes bounded context-bound credentials for media and operation-owned target payloads. +public static class DomainJoinCredentialPayloadCodec +{ + private static readonly UTF8Encoding Encoding = new(false, true); + private static ReadOnlySpan Magic => "FNDRYDJ1"u8; + + /// Returns an owned version-1 binary payload. The caller must clear the returned plaintext bytes. + public static byte[] Encode(DomainJoinCredentialContext context, ReadOnlySpan password) + { + ValidateContext(context); + ValidatePassword(password); + string domain = DomainJoinCredentialContext.CanonicalizeDomainName(context.DomainName); + string account = context.AccountName.Trim(); + int domainLength = Encoding.GetByteCount(domain); + int accountLength = Encoding.GetByteCount(account); + int passwordLength = Encoding.GetByteCount(password); + byte[] bytes = new byte[20 + domainLength + accountLength + passwordLength]; + try + { + Magic.CopyTo(bytes); + BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(8), domainLength); + BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(12), accountLength); + BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(16), passwordLength); + Encoding.GetBytes(domain, bytes.AsSpan(20, domainLength)); + Encoding.GetBytes(account, bytes.AsSpan(20 + domainLength, accountLength)); + Encoding.GetBytes(password, bytes.AsSpan(20 + domainLength + accountLength)); + return bytes; + } + catch + { + CryptographicOperations.ZeroMemory(bytes); + throw; + } + } + + /// Validates structure and authenticated inner context before returning any password. Media readers must supply the outer expected context. + public static DomainJoinCredentialPayload Decode(ReadOnlySpan bytes, DomainJoinCredentialContext? expected = null) + { + if (bytes.Length < 20 || bytes.Length > DomainJoinConfigurationValidator.MaximumCredentialPayloadBytes || !bytes[..8].SequenceEqual(Magic)) + throw new InvalidDataException("The domain credential payload is invalid."); + int domainLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[8..]); + int accountLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[12..]); + int passwordLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[16..]); + if (domainLength is <= 0 or > 1012 || accountLength is <= 0 or > 2048 || + passwordLength is <= 0 or > DomainJoinConfigurationValidator.MaximumPasswordUtf8Bytes || + 20L + domainLength + accountLength + passwordLength != bytes.Length) + throw new InvalidDataException("The domain credential payload is invalid."); + char[]? password = null; + try + { + var context = new DomainJoinCredentialContext(Encoding.GetString(bytes.Slice(20, domainLength)), + Encoding.GetString(bytes.Slice(20 + domainLength, accountLength))); + ValidateContext(context); + if (expected is not null && !context.Matches(expected)) + throw new CryptographicException("The domain credential context does not match."); + password = new char[Encoding.GetCharCount(bytes.Slice(20 + domainLength + accountLength, passwordLength))]; + Encoding.GetChars(bytes.Slice(20 + domainLength + accountLength, passwordLength), password); + ValidatePassword(password); + return new(context, password); + } + catch (DecoderFallbackException) + { + Clear(password); + throw new InvalidDataException("The domain credential payload is invalid."); + } + catch + { + Clear(password); + throw; + } + } + + internal static void ValidateContext(DomainJoinCredentialContext context) + { + ArgumentNullException.ThrowIfNull(context); + if (!DomainJoinCredentialContext.IsValidDomainName(context.DomainName) || + !DomainJoinConfigurationValidator.IsQualifiedAccount(context.AccountName)) + throw new InvalidDataException("The domain credential context is invalid."); + } + + internal static void ValidatePassword(ReadOnlySpan password) + { + try + { + if (password.IsEmpty || password.Contains('\0') || Encoding.GetByteCount(password) > DomainJoinConfigurationValidator.MaximumPasswordUtf8Bytes) + throw new InvalidDataException("The domain credential password is invalid."); + } + catch (EncoderFallbackException) + { + throw new InvalidDataException("The domain credential password is invalid."); + } + } + + private static void Clear(char[]? password) + { + if (password is not null) CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); + } +} + +/// Owns decoded plaintext; disposal clears its password memory. The context contains no password. +public sealed class DomainJoinCredentialPayload : IDisposable +{ + private readonly char[] password; + private bool isDisposed; + internal DomainJoinCredentialPayload(DomainJoinCredentialContext context, char[] password) + { + Context = context; + this.password = password; + } + public DomainJoinCredentialContext Context { get; } + /// Exposes owned memory valid only until disposal; consumers must not retain plaintext copies. + public ReadOnlyMemory Password + { + get + { + ObjectDisposedException.ThrowIf(isDisposed, this); + return password; + } + } + public void Dispose() + { + if (isDisposed) return; + CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); + isDisposed = true; + } +} diff --git a/src/Foundry.Core/Services/Configuration/DomainJoinSecretState.cs b/src/Foundry.Core/Services/Configuration/DomainJoinSecretState.cs new file mode 100644 index 000000000..0d026eaad --- /dev/null +++ b/src/Foundry.Core/Services/Configuration/DomainJoinSecretState.cs @@ -0,0 +1,70 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Foundry.Core.Models.Configuration; + +namespace Foundry.Core.Services.Configuration; + +/// Owns one session password bound to an active automatic domain credential context. +public sealed class DomainJoinSecretState : IDisposable +{ + private char[]? password; + private DomainJoinCredentialContext? context; + private bool isDisposed; + + /// Validates and copies a password without normalization, clearing the previous owned buffer. + public void SetPassword(DomainJoinCredentialContext context, ReadOnlySpan value) + { + ObjectDisposedException.ThrowIf(isDisposed, this); + DomainJoinCredentialPayloadCodec.ValidateContext(context); + if (!value.IsEmpty) DomainJoinCredentialPayloadCodec.ValidatePassword(value); + Clear(); + this.context = context; + password = value.IsEmpty ? null : value.ToArray(); + } + + /// Returns an independent owned copy only for matching identity; callers must clear it. + public char[]? GetPasswordCopy(DomainJoinCredentialContext context) + { + ObjectDisposedException.ThrowIf(isDisposed, this); + return HasPassword(context) ? password!.ToArray() : null; + } + + /// Reports availability only for the same canonical domain and account. + public bool HasPassword(DomainJoinCredentialContext context) + { + ObjectDisposedException.ThrowIf(isDisposed, this); + ArgumentNullException.ThrowIfNull(context); + return password is { Length: > 0 } && this.context?.Matches(context) == true; + } + + /// Clears credentials immediately when automatic mode or their domain/account ownership changes. + public bool Update(DomainJoinSettings settings) + { + ObjectDisposedException.ThrowIf(isDisposed, this); + ArgumentNullException.ThrowIfNull(settings); + return !settings.IsEnabled || settings.Mode != DomainJoinMode.Automatic || + context?.Matches(new(settings.DomainName ?? string.Empty, settings.AccountName ?? string.Empty)) != true + ? Clear() : false; + } + + /// Erases the owned password and its binding; independent caller copies remain caller-owned. + public bool Clear() + { + bool changed = password is not null; + if (password is not null) CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); + password = null; + context = null; + return changed; + } + + public void Dispose() + { + if (isDisposed) return; + Clear(); + isDisposed = true; + } +} diff --git a/src/Foundry.Core/Services/Configuration/IDeployConfigurationGenerator.cs b/src/Foundry.Core/Services/Configuration/IDeployConfigurationGenerator.cs index 743c104da..8f555cced 100644 --- a/src/Foundry.Core/Services/Configuration/IDeployConfigurationGenerator.cs +++ b/src/Foundry.Core/Services/Configuration/IDeployConfigurationGenerator.cs @@ -55,6 +55,15 @@ FoundryDeployConfigurationDocument Generate( OobeAccountSecretState? oobeAccountSecretState, string? authoringVersion = null); + /// Generates active domain metadata and context-bound credentials using the existing media protection. + FoundryDeployConfigurationDocument Generate( + FoundryConfigurationDocument document, + byte[]? deploymentSecretsKey, + DeployProtectionSettings? protectionSettings, + OobeAccountSecretState? oobeAccountSecretState, + DomainJoinSecretState? domainJoinSecretState, + string? authoringVersion = null); + /// /// Serializes a deployment runtime configuration document to JSON. /// diff --git a/src/Foundry.Core/Services/Profiles/DeploymentBuildSnapshot.cs b/src/Foundry.Core/Services/Profiles/DeploymentBuildSnapshot.cs index 9f1a0b64f..33f3e8f90 100644 --- a/src/Foundry.Core/Services/Profiles/DeploymentBuildSnapshot.cs +++ b/src/Foundry.Core/Services/Profiles/DeploymentBuildSnapshot.cs @@ -24,6 +24,7 @@ public sealed class DeploymentBuildSnapshot : IDisposable private const int MaximumDriverEntries = 10_000; private const string LeaseFileName = ".lease"; private readonly OobeAccountSecretState accountSecrets = new(); + private readonly DomainJoinSecretState domainSecrets = new(); private readonly char[] deploymentPassword; private readonly char[]? wifiPassphrase; private readonly char[]? wiredCertificatePassword; @@ -66,6 +67,17 @@ public static Task CaptureAsync( ReadOnlySpan deploymentPassword, string privateRootDirectory, string? authoringVersion, + CancellationToken cancellationToken = default) => + CaptureAsync(configuration, accountSecrets, null, deploymentPassword, privateRootDirectory, authoringVersion, cancellationToken); + + /// Copies active domain credentials synchronously before source preparation can await or authoring can change. + public static Task CaptureAsync( + FoundryConfigurationDocument configuration, + OobeAccountSecretState accountSecrets, + DomainJoinSecretState? domainSecrets, + ReadOnlySpan deploymentPassword, + string privateRootDirectory, + string? authoringVersion, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(configuration); @@ -75,6 +87,12 @@ public static Task CaptureAsync( try { snapshot.CopyAccountSecrets(accountSecrets); + if (domainSecrets is not null && configuration.DomainJoin.IsEnabled && configuration.DomainJoin.Mode == DomainJoinMode.Automatic) + { + var context = new DomainJoinCredentialContext(configuration.DomainJoin.DomainName ?? string.Empty, configuration.DomainJoin.AccountName ?? string.Empty); + char[]? password = domainSecrets.GetPasswordCopy(context); + if (password is not null) Copy(password, value => snapshot.domainSecrets.SetPassword(context, value)); + } return snapshot.PrepareAsync(cancellationToken); } catch @@ -100,7 +118,7 @@ public string GenerateDeployConfigurationJson(TelemetrySettings? telemetryOverri throw new InvalidOperationException("OOBE local account password confirmation is invalid."); } var generator = new DeployConfigurationGenerator(); - return generator.Serialize(generator.Generate(CreateGenerationDocument(telemetryOverride), deploymentSecretsKey, protectionSettings, accountSecrets, authoringVersion)); + return generator.Serialize(generator.Generate(CreateGenerationDocument(telemetryOverride), deploymentSecretsKey, protectionSettings, accountSecrets, domainSecrets, authoringVersion)); } /// Creates fresh per-media protection using the confirmed password captured before preparation began. @@ -119,6 +137,7 @@ public void Dispose() { isDisposed = true; accountSecrets.Dispose(); + domainSecrets.Dispose(); Clear(deploymentPassword); Clear(wifiPassphrase); Clear(wiredCertificatePassword); diff --git a/src/Foundry.Core/Services/Profiles/DeploymentProfileMerge.cs b/src/Foundry.Core/Services/Profiles/DeploymentProfileMerge.cs index 430affcf2..774008346 100644 --- a/src/Foundry.Core/Services/Profiles/DeploymentProfileMerge.cs +++ b/src/Foundry.Core/Services/Profiles/DeploymentProfileMerge.cs @@ -54,6 +54,20 @@ public static DeploymentProfileDocument PreserveOmittedSourceMetadata(Deployment }); } } + DomainJoinSettings domain = captured.Configuration.DomainJoin; + if (domain.IsEnabled && domain.Mode == DomainJoinMode.Automatic) + { + string identity = DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, captured); + DeploymentProfileSecret? omitted = baseline.Secrets.Entries.SingleOrDefault(secret => secret.Purpose == ProfileSecretPurpose.DomainJoinPassword && + secret.Identity == identity && secret.State == ProfileValueState.Omitted); + if (omitted is not null) + { + int index = secrets.FindIndex(secret => secret.Purpose == ProfileSecretPurpose.DomainJoinPassword); + if (index < 0) secrets.Add(omitted with { Value = null }); + else if (secrets[index].State == ProfileValueState.Unavailable) + secrets[index] = secrets[index] with { State = ProfileValueState.Omitted, Value = null }; + } + } return result with { Secrets = new() { Entries = secrets } }; } @@ -106,7 +120,12 @@ public static DeploymentProfileDocument PreserveOmittedLocalValues(DeploymentPro Entries = incoming.Secrets.Entries.Select(secret => { DeploymentProfileSecret selected = secret; - if (secret.State == ProfileValueState.Omitted) + bool matchingDomain = secret.Purpose != ProfileSecretPurpose.DomainJoinPassword || + incoming.Configuration.DomainJoin.IsEnabled && incoming.Configuration.DomainJoin.Mode == DomainJoinMode.Automatic && + new DomainJoinCredentialContext(incoming.Configuration.DomainJoin.DomainName ?? string.Empty, incoming.Configuration.DomainJoin.AccountName ?? string.Empty) + .Matches(new(local.Configuration.DomainJoin.DomainName ?? string.Empty, local.Configuration.DomainJoin.AccountName ?? string.Empty)) && + secret.Identity == DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, incoming); + if (secret.State == ProfileValueState.Omitted && matchingDomain) { selected = local.Secrets.Entries.SingleOrDefault(candidate => candidate.Purpose == secret.Purpose && candidate.Identity == secret.Identity && candidate.State is ProfileValueState.Present or ProfileValueState.Blank) ?? secret; diff --git a/src/Foundry.Core/Services/Profiles/DeploymentProfilePackageService.cs b/src/Foundry.Core/Services/Profiles/DeploymentProfilePackageService.cs index 6334c9db6..cf5378729 100644 --- a/src/Foundry.Core/Services/Profiles/DeploymentProfilePackageService.cs +++ b/src/Foundry.Core/Services/Profiles/DeploymentProfilePackageService.cs @@ -29,7 +29,7 @@ public sealed class DeploymentProfilePackageService : IDeploymentProfilePackageS private const int Iterations = 600_000; private static ReadOnlySpan Magic => "FNDRYPRF"u8; - /// Creates an encrypted portable export. Only explicitly supplied logical secrets and assets are included. + /// Creates an encrypted portable export. Domain passwords are always omitted; other explicitly supplied logical secrets and assets are included. public byte[] Export(DeploymentProfileDocument profile, ReadOnlySpan passphrase) { ValidatePassphrase(passphrase); @@ -37,7 +37,15 @@ public byte[] Export(DeploymentProfileDocument profile, ReadOnlySpan passp byte[] key = PasswordKeyDerivation.DeriveKey(passphrase, header.AsSpan(14, 16), Iterations, 32); try { - return EncryptPayload(profile, key, header, [], true); + DeploymentProfileDocument exported = profile with + { + Secrets = new() + { + Entries = profile.Secrets.Entries.Select(secret => secret.Purpose == ProfileSecretPurpose.DomainJoinPassword + ? secret with { State = ProfileValueState.Omitted, Value = null } : secret).ToArray() + } + }; + return EncryptPayload(exported, key, header, [], true); } finally { diff --git a/src/Foundry.Core/Services/Profiles/DeploymentProfilePayload.cs b/src/Foundry.Core/Services/Profiles/DeploymentProfilePayload.cs index 35f9dc27d..d86508618 100644 --- a/src/Foundry.Core/Services/Profiles/DeploymentProfilePayload.cs +++ b/src/Foundry.Core/Services/Profiles/DeploymentProfilePayload.cs @@ -120,7 +120,8 @@ private static void Validate(DeploymentProfileDocument profile) { if (secret is null || !Enum.IsDefined(secret.Purpose) || !Enum.IsDefined(secret.State) || !IsIdentity(secret.Identity) || !identities.Add($"{(int)secret.Purpose}:{secret.Identity}") - || (secret.State == ProfileValueState.Present ? !IsValidSecret(secret.Value) : secret.Value is not null)) + || (secret.State == ProfileValueState.Present ? !IsValidSecret(secret.Value) : secret.Value is not null) + || (secret.Purpose == ProfileSecretPurpose.DomainJoinPassword && secret.Value?.Contains((byte)0) == true)) { throw new InvalidDataException("The profile secret record is invalid."); } @@ -199,6 +200,7 @@ private static void ValidateConfigurationCollections(FoundryConfigurationDocumen ValidateList(configuration.OperatingSystemSelection.AllowedEditions); ValidateList(configuration.Autopilot.HardwareHashUpload.KnownGroupTags); ValidateList(configuration.Autopilot.Profiles); + ValidateList(configuration.DomainJoin.OrganizationalUnits); ValidateList(configuration.Unattend.Files); var ids = new HashSet(StringComparer.OrdinalIgnoreCase); var folders = new HashSet(StringComparer.OrdinalIgnoreCase); diff --git a/src/Foundry.Core/Services/Profiles/DeploymentProfileSecretBinding.cs b/src/Foundry.Core/Services/Profiles/DeploymentProfileSecretBinding.cs index 771307cbc..ffe4cd4e4 100644 --- a/src/Foundry.Core/Services/Profiles/DeploymentProfileSecretBinding.cs +++ b/src/Foundry.Core/Services/Profiles/DeploymentProfileSecretBinding.cs @@ -19,6 +19,8 @@ public static string Identity(ProfileSecretPurpose purpose, DeploymentProfileDoc FoundryConfigurationDocument configuration = profile.Configuration; string[] context = purpose switch { + ProfileSecretPurpose.DomainJoinPassword => [DomainJoinCredentialContext.CanonicalizeDomainName(configuration.DomainJoin.DomainName), + DomainJoinCredentialContext.CanonicalizeAccountName(configuration.DomainJoin.AccountName)], ProfileSecretPurpose.DeploymentPassword => ["media-protection"], ProfileSecretPurpose.WifiPassphrase => [configuration.Network.Wifi.Ssid ?? string.Empty, configuration.Network.Wifi.SecurityType ?? string.Empty], ProfileSecretPurpose.AdministratorPassword => ["builtin-administrator"], diff --git a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs index cdacd54be..12e7a84c2 100644 --- a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs @@ -121,6 +121,7 @@ public static IServiceCollection AddFoundryApplicationServices(this IServiceColl services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Foundry/Services/Configuration/DeploymentProfileCoordinator.cs b/src/Foundry/Services/Configuration/DeploymentProfileCoordinator.cs index 262a5135b..405849cfc 100644 --- a/src/Foundry/Services/Configuration/DeploymentProfileCoordinator.cs +++ b/src/Foundry/Services/Configuration/DeploymentProfileCoordinator.cs @@ -18,6 +18,7 @@ public sealed partial class DeploymentProfileCoordinator : IDisposable private readonly IDeploymentProfilePackageService packages; private readonly DeploymentProfileSessionService session; private readonly IFoundryConfigurationStateService configuration; + private readonly IDomainJoinSecretStateService domainSecrets; private readonly IAppDispatcher dispatcher; private readonly IShellNavigationGuardService navigationGuard; private readonly SemaphoreSlim gate = new(1, 1); @@ -61,12 +62,13 @@ public IDisposable SuspendActivation() public DeploymentProfileCoordinator(LocalDeploymentProfileRepository local, IDeploymentProfilePackageService packages, DeploymentProfileSessionService session, IFoundryConfigurationStateService configuration, IAppDispatcher dispatcher, IShellNavigationGuardService navigationGuard, - INetworkSecretStateService network, IDeploymentProtectionSecretStateService deployment, IOobeAccountSecretStateService accounts) + INetworkSecretStateService network, IDeploymentProtectionSecretStateService deployment, IOobeAccountSecretStateService accounts, IDomainJoinSecretStateService domainSecrets) { this.local = local; this.packages = packages; this.session = session; this.configuration = configuration; + this.domainSecrets = domainSecrets; observedConfiguration = configuration.Current; this.dispatcher = dispatcher; this.navigationGuard = navigationGuard; @@ -74,6 +76,7 @@ public DeploymentProfileCoordinator(LocalDeploymentProfileRepository local, IDep network.Changed += OnEdited; deployment.Changed += OnEdited; accounts.Changed += OnEdited; + domainSecrets.Changed += OnEdited; } public event EventHandler? Changed; @@ -500,6 +503,7 @@ private static bool HasSharedConfigurationChanges( !ReferenceEquals(previous.PreOobe, current.PreOobe) || !ReferenceEquals(previous.Unattend, current.Unattend) || !ReferenceEquals(previous.Autopilot, current.Autopilot) || + !ReferenceEquals(previous.DomainJoin, current.DomainJoin) || previous.General with { IsoOutputPath = current.General.IsoOutputPath, @@ -577,11 +581,11 @@ private void ClearSharedKey() private static void EnsureCompleteCheckpoint(DeploymentProfileDocument profile) { - if (profile.Secrets.Entries.Any(secret => secret.State is ProfileValueState.Unavailable or ProfileValueState.Omitted) || + if (profile.Secrets.Entries.Any(secret => secret.Purpose != ProfileSecretPurpose.DomainJoinPassword && (secret.State is ProfileValueState.Unavailable or ProfileValueState.Omitted)) || profile.Assets.Any(asset => asset.State is ProfileValueState.Unavailable or ProfileValueState.Omitted)) { Logger.Warning("Profile checkpoint is incomplete. UnavailableSecrets={UnavailableSecrets}, UnavailableAssets={UnavailableAssets}", - profile.Secrets.Entries.Count(secret => secret.State is ProfileValueState.Unavailable or ProfileValueState.Omitted), + profile.Secrets.Entries.Count(secret => secret.Purpose != ProfileSecretPurpose.DomainJoinPassword && (secret.State is ProfileValueState.Unavailable or ProfileValueState.Omitted)), profile.Assets.Count(asset => asset.State is ProfileValueState.Unavailable or ProfileValueState.Omitted)); throw new IncompleteProfileCheckpointException(); } @@ -615,6 +619,7 @@ public void Dispose() { // A close scope may outlive host disposal; it must not re-enable the destroyed shell. ownsNavigationGuard = false; + domainSecrets.Changed -= OnEdited; lifetime.Cancel(); debounce?.Cancel(); debounce?.Dispose(); diff --git a/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs b/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs index 08c0adae5..8d00352e3 100644 --- a/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs +++ b/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs @@ -23,6 +23,7 @@ public sealed class DeploymentProfileSessionService : IDisposable private readonly INetworkSecretStateService networkSecrets; private readonly IDeploymentProtectionSecretStateService deploymentSecrets; private readonly IOobeAccountSecretStateService accountSecrets; + private readonly IDomainJoinSecretStateService domainSecrets; private readonly IAutopilotHardwareHashSessionState autopilotSession; private DeploymentProfileDocument? sourceMetadata; private FoundryConfigurationDocument? materializedSourceMetadata; @@ -32,12 +33,14 @@ public DeploymentProfileSessionService( INetworkSecretStateService networkSecrets, IDeploymentProtectionSecretStateService deploymentSecrets, IOobeAccountSecretStateService accountSecrets, + IDomainJoinSecretStateService domainSecrets, IAutopilotHardwareHashSessionState autopilotSession) { this.configurationState = configurationState; this.networkSecrets = networkSecrets; this.deploymentSecrets = deploymentSecrets; this.accountSecrets = accountSecrets; + this.domainSecrets = domainSecrets; this.autopilotSession = autopilotSession; configurationState.StateChanged += OnConfigurationChanged; } @@ -76,6 +79,25 @@ public Task CaptureAsync(Guid profileId, string displ Add(ProfileSecretPurpose.WifiPassphrase, configuration.Network.Wifi.Passphrase); } + if (configuration.DomainJoin.IsEnabled && configuration.DomainJoin.Mode == DomainJoinMode.Automatic) + { + var context = new DomainJoinCredentialContext(configuration.DomainJoin.DomainName ?? string.Empty, configuration.DomainJoin.AccountName ?? string.Empty); + char[]? password = domainSecrets.GetPasswordCopy(context); + try + { + entries.Add((new DeploymentProfileSecret + { + Purpose = ProfileSecretPurpose.DomainJoinPassword, + State = !includeSecrets ? ProfileValueState.Omitted : password is null ? ProfileValueState.Unavailable : ProfileValueState.Present, + Value = includeSecrets && password is not null ? EncodePassword(password) : null + }, null)); + } + finally + { + if (password is not null) CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); + } + } + OobeSettings oobe = configuration.Customization.Oobe; if (oobe.IsEnabled) { @@ -195,6 +217,13 @@ void AddPair(ProfileSecretPurpose purpose, char[] password, char[] confirmation, } } + private static byte[] EncodePassword(ReadOnlySpan password) + { + byte[] bytes = new byte[Encoding.UTF8.GetByteCount(password)]; + Encoding.UTF8.GetBytes(password, bytes); + return bytes; + } + private static FoundryConfigurationDocument FreezeConfiguration(FoundryConfigurationDocument source) { AutopilotBootMediaCertificateSettings certificate = source.Autopilot.HardwareHashUpload.BootMediaCertificate with { PfxPassword = null }; @@ -224,6 +253,7 @@ public void ClearSensitiveState() networkSecrets.Update(new NetworkSettings()); deploymentSecrets.Clear(); accountSecrets.Update(new OobeSettings()); + domainSecrets.Clear(); autopilotSession.ClearTenantConnection(); autopilotSession.BootMediaCertificate = new(); Logger.Debug("Profile session credentials cleared."); @@ -266,23 +296,45 @@ public void Activate(DeploymentProfileDocument profile, FoundryConfigurationDocu } } - configurationState.Replace(materialized, () => + char[]? domainPassword = GetDomainPassword(); + try { - ClearSensitiveState(); - deploymentSecrets.SetPassword(deploymentPassword); - deploymentSecrets.SetConfirmation(deploymentPassword); - accountSecrets.SetAdministratorPassword(administratorPassword); - accountSecrets.SetAdministratorConfirmation(administratorPassword); - foreach ((string id, string? password) in additionalPasswords) + configurationState.Replace(materialized, () => { - accountSecrets.SetAdditionalAccountPassword(id, password.AsSpan()); - accountSecrets.SetAdditionalAccountConfirmation(id, password.AsSpan()); - } - autopilotSession.BootMediaCertificate = boot; - }); + ClearSensitiveState(); + deploymentSecrets.SetPassword(deploymentPassword); + deploymentSecrets.SetConfirmation(deploymentPassword); + accountSecrets.SetAdministratorPassword(administratorPassword); + accountSecrets.SetAdministratorConfirmation(administratorPassword); + foreach ((string id, string? password) in additionalPasswords) + { + accountSecrets.SetAdditionalAccountPassword(id, password.AsSpan()); + accountSecrets.SetAdditionalAccountConfirmation(id, password.AsSpan()); + } + if (domainPassword is not null) + domainSecrets.SetPassword(new(materialized.DomainJoin.DomainName!, materialized.DomainJoin.AccountName!), domainPassword); + autopilotSession.BootMediaCertificate = boot; + }); + } + finally + { + if (domainPassword is not null) CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(domainPassword.AsSpan())); + } RememberSourceMetadata(profile, materialized); Logger.Information("Profile session activation completed. ProfileId={ProfileId}", profile.ProfileId); + char[]? GetDomainPassword() + { + DomainJoinSettings settings = materialized.DomainJoin; + if (!settings.IsEnabled || settings.Mode != DomainJoinMode.Automatic) return null; + var context = new DomainJoinCredentialContext(settings.DomainName ?? string.Empty, settings.AccountName ?? string.Empty); + if (!context.Matches(new(profile.Configuration.DomainJoin.DomainName ?? string.Empty, profile.Configuration.DomainJoin.AccountName ?? string.Empty))) return null; + string identity = DeploymentProfileSecretBinding.Identity(ProfileSecretPurpose.DomainJoinPassword, profile); + DeploymentProfileSecret? secret = profile.Secrets.Entries.SingleOrDefault(secret => secret.Purpose == ProfileSecretPurpose.DomainJoinPassword && secret.Identity == identity); + if (secret?.State != ProfileValueState.Present) return null; + return Encoding.UTF8.GetChars(secret.Value!); + } + string? Get(ProfileSecretPurpose purpose, string? accountId = null) { string identity = DeploymentProfileSecretBinding.Identity(purpose, profile, accountId); @@ -328,6 +380,7 @@ private void RememberSourceMetadata(DeploymentProfileDocument profile, FoundryCo private void OnConfigurationChanged(object? sender, EventArgs e) { + domainSecrets.Update(configurationState.Current.DomainJoin); if (sourceMetadata is null || materializedSourceMetadata is null) return; FoundryConfigurationDocument current = configurationState.Current with { diff --git a/src/Foundry/Services/Configuration/DomainJoinSecretStateService.cs b/src/Foundry/Services/Configuration/DomainJoinSecretStateService.cs new file mode 100644 index 000000000..b64e037f1 --- /dev/null +++ b/src/Foundry/Services/Configuration/DomainJoinSecretStateService.cs @@ -0,0 +1,31 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; + +namespace Foundry.Services.Configuration; + +/// Owns the desktop session's domain password and publishes changes to profile persistence. +internal sealed class DomainJoinSecretStateService : IDomainJoinSecretStateService, IDisposable +{ + private readonly DomainJoinSecretState state = new(); + public event EventHandler? Changed; + public void SetPassword(DomainJoinCredentialContext context, ReadOnlySpan value) + { + state.SetPassword(context, value); + Changed?.Invoke(this, EventArgs.Empty); + } + public char[]? GetPasswordCopy(DomainJoinCredentialContext context) => state.GetPasswordCopy(context); + public bool HasPassword(DomainJoinCredentialContext context) => state.HasPassword(context); + public void Update(DomainJoinSettings settings) + { + if (state.Update(settings)) Changed?.Invoke(this, EventArgs.Empty); + } + public void Clear() + { + if (state.Clear()) Changed?.Invoke(this, EventArgs.Empty); + } + public void Dispose() => state.Dispose(); +} diff --git a/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs b/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs index a99df3b0c..1d6264f7d 100644 --- a/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs +++ b/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs @@ -31,6 +31,7 @@ internal sealed class FoundryConfigurationStateService : IFoundryConfigurationSt private readonly INetworkSecretStateService networkSecretStateService; private readonly IDeploymentProtectionSecretStateService deploymentProtectionSecretStateService; private readonly IOobeAccountSecretStateService oobeAccountSecretStateService; + private readonly IDomainJoinSecretStateService domainJoinSecretStateService; private readonly IAutopilotHardwareHashSessionState autopilotHardwareHashSessionState; private readonly AppSettingsService appSettingsService; private readonly ILogger logger; @@ -47,6 +48,7 @@ public FoundryConfigurationStateService( INetworkSecretStateService networkSecretStateService, IDeploymentProtectionSecretStateService deploymentProtectionSecretStateService, IOobeAccountSecretStateService oobeAccountSecretStateService, + IDomainJoinSecretStateService domainJoinSecretStateService, IAutopilotHardwareHashSessionState autopilotHardwareHashSessionState, AppSettingsService appSettingsService, ILogger logger, @@ -58,6 +60,7 @@ public FoundryConfigurationStateService( this.networkSecretStateService = networkSecretStateService; this.deploymentProtectionSecretStateService = deploymentProtectionSecretStateService; this.oobeAccountSecretStateService = oobeAccountSecretStateService; + this.domainJoinSecretStateService = domainJoinSecretStateService; this.autopilotHardwareHashSessionState = autopilotHardwareHashSessionState; this.appSettingsService = appSettingsService; this.logger = logger.ForContext(); @@ -89,6 +92,7 @@ public void Replace(FoundryConfigurationDocument document, Action restoreSecrets restoreSecrets(); networkSecretStateService.Update(document.Network); oobeAccountSecretStateService.Update(document.Customization.Oobe); + domainJoinSecretStateService.Update(document.DomainJoin); Current = candidate; validatedUnattendSettings = null; unattendSourceValidations = []; @@ -434,9 +438,10 @@ private string GenerateDeployConfigurationJson( } using OobeAccountSecretState oobeAccountSecretState = CreateOobeAccountSecretStateForDeployGeneration(document.Customization.Oobe); + using DomainJoinSecretState domainSecrets = CreateDomainJoinSecretState(document.DomainJoin); return deployConfigurationGenerator.Serialize( - deployConfigurationGenerator.Generate(document, deploymentSecretsKey, protectionSettings, oobeAccountSecretState)); + deployConfigurationGenerator.Generate(document, deploymentSecretsKey, protectionSettings, oobeAccountSecretState, domainSecrets)); } /// @@ -450,13 +455,14 @@ public Task CaptureBuildSnapshotAsync( Network = networkSecretStateService.ApplyRequiredSecrets(Current.Network) }; using OobeAccountSecretState accountSecrets = CreateOobeAccountSecretStateForDeployGeneration(document.Customization.Oobe); + using DomainJoinSecretState domainSecrets = CreateDomainJoinSecretState(document.DomainJoin); char[] password = document.General.DeploymentProtection.IsEnabled ? deploymentProtectionSecretStateService.GetConfirmedPasswordCopy() : []; try { return DeploymentBuildSnapshot.CaptureAsync( - document, accountSecrets, password, privateRootDirectory, authoringVersion, cancellationToken); + document, accountSecrets, domainSecrets, password, privateRootDirectory, authoringVersion, cancellationToken); } finally { @@ -464,6 +470,28 @@ public Task CaptureBuildSnapshotAsync( } } + private DomainJoinSecretState CreateDomainJoinSecretState(DomainJoinSettings settings) + { + var state = new DomainJoinSecretState(); + if (!settings.IsEnabled || settings.Mode != DomainJoinMode.Automatic) return state; + var context = new DomainJoinCredentialContext(settings.DomainName ?? string.Empty, settings.AccountName ?? string.Empty); + char[]? password = domainJoinSecretStateService.GetPasswordCopy(context); + try + { + if (password is not null) state.SetPassword(context, password); + return state; + } + catch + { + state.Dispose(); + throw; + } + finally + { + if (password is not null) CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); + } + } + private FoundryConfigurationDocument Load(out bool isLegacyMigration) { isLegacyMigration = false; diff --git a/src/Foundry/Services/Configuration/IDomainJoinSecretStateService.cs b/src/Foundry/Services/Configuration/IDomainJoinSecretStateService.cs new file mode 100644 index 000000000..cede6b7a4 --- /dev/null +++ b/src/Foundry/Services/Configuration/IDomainJoinSecretStateService.cs @@ -0,0 +1,19 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; + +namespace Foundry.Services.Configuration; + +/// Provides volatile context-bound domain credentials without persisting them in authoring configuration. +public interface IDomainJoinSecretStateService +{ + event EventHandler? Changed; + void SetPassword(DomainJoinCredentialContext context, ReadOnlySpan value); + /// Returns an owned copy for matching identity. The caller must clear the buffer. + char[]? GetPasswordCopy(DomainJoinCredentialContext context); + bool HasPassword(DomainJoinCredentialContext context); + void Update(DomainJoinSettings settings); + void Clear(); +} From 9e96d757b0cabae6c9ba95811a5e9eceed33dd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Sun, 4 Oct 2026 15:29:25 +0200 Subject: [PATCH 03/84] test(profiles): restore Unicode asset fixture --- .../Profiles/DeploymentProfilePackageServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs b/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs index a8f5e9384..a42638e84 100644 --- a/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs +++ b/src/Foundry.Core.Tests/Profiles/DeploymentProfilePackageServiceTests.cs @@ -432,7 +432,7 @@ public void Export_RejectsUnsafeInlineAutopilotFolder(string folder) [Fact] public void ExportImport_PreservesSafeUnicodeAutopilotFolderAndInlineJson() { - AutopilotProfileSettings autopilot = CreateAutopilotProfile() with { FolderName = "Équipe_Paris" }; + AutopilotProfileSettings autopilot = CreateAutopilotProfile() with { FolderName = "Équipe_Paris" }; DeploymentProfileDocument profile = CreateProfile() with { Configuration = new() { Autopilot = new() { Profiles = [autopilot] } } }; AutopilotProfileSettings imported = Assert.Single(_service.Import(_service.Export(profile, "password"), "password").Configuration.Autopilot.Profiles); Assert.Equal(autopilot.FolderName, imported.FolderName); From 0b8b5387c91188ac9bac73333d6e4e352a61b9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Sun, 4 Oct 2026 15:47:44 +0200 Subject: [PATCH 04/84] feat(osd): add domain join pages and destination OU configuration --- ...omainJoinOrganizationalUnitCatalogTests.cs | 109 +++++++ .../DomainJoinOrganizationalUnitCatalog.cs | 70 +++++ .../ServiceCollectionExtensions.cs | 2 + src/Foundry/Foundry.csproj | 3 +- src/Foundry/FoundryApplicationInfo.cs | 6 + .../ConfigurationOverviewService.cs | 5 + .../FoundryConfigurationStateService.cs | 24 +- .../IFoundryConfigurationStateService.cs | 9 + .../AuthoringDomainOuDiscoveryService.cs | 219 ++++++++++++++ .../IAuthoringDomainOuDiscoveryService.cs | 22 ++ .../Services/Shell/NavigationRouteCatalog.cs | 6 +- .../Services/Shell/NavigationStatusService.cs | 5 +- src/Foundry/Strings/ar-SA/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/bg-BG/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/cs-CZ/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/da-DK/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/de-DE/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/el-GR/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/en-GB/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/en-US/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/es-ES/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/es-MX/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/et-EE/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/fi-FI/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/fr-CA/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/fr-FR/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/he-IL/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/hr-HR/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/hu-HU/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/it-IT/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/ja-JP/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/ko-KR/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/lt-LT/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/lv-LV/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/nb-NO/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/nl-NL/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/pl-PL/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/pt-BR/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/pt-PT/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/ro-RO/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/ru-RU/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/sk-SK/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/sl-SI/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/sr-Latn-RS/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/sv-SE/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/th-TH/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/tr-TR/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/uk-UA/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/zh-CN/Resources.resw | 204 +++++++++++++ src/Foundry/Strings/zh-TW/Resources.resw | 204 +++++++++++++ .../AutopilotConfigurationViewModel.cs | 37 +-- .../DomainJoinConfigurationViewModel.cs | 284 ++++++++++++++++++ ...ainJoinOrganizationalUnitEntryViewModel.cs | 30 ++ src/Foundry/ViewModels/StartMediaViewModel.cs | 17 ++ .../Views/InteractiveDomainJoinPage.xaml | 155 ++++++++++ .../Views/InteractiveDomainJoinPage.xaml.cs | 35 +++ src/Foundry/Views/StartPage.xaml | 15 + src/Foundry/Views/StartPage.xaml.cs | 7 + .../Views/ZeroTouchDomainJoinPage.xaml | 169 +++++++++++ .../Views/ZeroTouchDomainJoinPage.xaml.cs | 65 ++++ 60 files changed, 9018 insertions(+), 28 deletions(-) create mode 100644 src/Foundry.Core.Tests/Configuration/DomainJoinOrganizationalUnitCatalogTests.cs create mode 100644 src/Foundry.Core/Services/Configuration/DomainJoinOrganizationalUnitCatalog.cs create mode 100644 src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs create mode 100644 src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs create mode 100644 src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs create mode 100644 src/Foundry/ViewModels/DomainJoinOrganizationalUnitEntryViewModel.cs create mode 100644 src/Foundry/Views/InteractiveDomainJoinPage.xaml create mode 100644 src/Foundry/Views/InteractiveDomainJoinPage.xaml.cs create mode 100644 src/Foundry/Views/ZeroTouchDomainJoinPage.xaml create mode 100644 src/Foundry/Views/ZeroTouchDomainJoinPage.xaml.cs diff --git a/src/Foundry.Core.Tests/Configuration/DomainJoinOrganizationalUnitCatalogTests.cs b/src/Foundry.Core.Tests/Configuration/DomainJoinOrganizationalUnitCatalogTests.cs new file mode 100644 index 000000000..03dee265c --- /dev/null +++ b/src/Foundry.Core.Tests/Configuration/DomainJoinOrganizationalUnitCatalogTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; + +namespace Foundry.Core.Tests.Configuration; + +public sealed class DomainJoinOrganizationalUnitCatalogTests +{ + [Fact] + public void CatalogMergePreservesManualRowsAndDefault() + { + DomainJoinSettings current = Catalog(); + DomainJoinSettings merged = DomainJoinOrganizationalUnitCatalog.Merge(current, "CONTOSO.TEST.", + [Unit("import", "Renamed", "ou=devices,dc=contoso,dc=test"), Unit("new", "Servers", "OU=Servers,DC=contoso,DC=test")]); + Assert.Equal("manual", merged.DefaultOuId); + Assert.Equal(2, merged.OrganizationalUnits.Count); + Assert.Equal(current.OrganizationalUnits[0], merged.OrganizationalUnits[0]); + Assert.Single(current.OrganizationalUnits); + } + + [Fact] + public void DomainChangeInvalidatesCatalogSelection() + { + DomainJoinSettings changed = Catalog() with { DomainName = "other.test" }; + Assert.Throws(() => DomainJoinOrganizationalUnitCatalog.Merge(changed, "other.test", [])); + Assert.False(DomainJoinConfigurationValidator.ValidateMetadata(changed).IsValid); + } + + [Fact] + public void ForeignDomainImportLeavesConfigurationUnchanged() + { + DomainJoinSettings current = Catalog(); + Assert.Throws(() => DomainJoinOrganizationalUnitCatalog.Merge(current, "other.test", + [Unit("foreign", "Foreign", "OU=Foreign,DC=other,DC=test")])); + Assert.Equal("contoso.test", current.DomainName); + Assert.Equal("manual", current.DefaultOuId); + Assert.Single(current.OrganizationalUnits); + } + + [Fact] + public void RemovingDefaultClearsDefault() + { + DomainJoinSettings removed = DomainJoinOrganizationalUnitCatalog.Remove(Catalog(), "MANUAL"); + Assert.Empty(removed.OrganizationalUnits); + Assert.Null(removed.DefaultOuId); + Assert.False(removed.AllowOuSelectionDuringDeployment); + Assert.Null(removed.OuCatalogDomain); + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(removed).IsValid); + } + + [Fact] + public void ImportRejectsDuplicateIdentityForDifferentDestination() + { + DomainJoinSettings current = Catalog(); + Assert.Throws(() => DomainJoinOrganizationalUnitCatalog.Merge(current, "contoso.test", + [Unit("manual", "Servers", "OU=Servers,DC=contoso,DC=test")])); + Assert.Single(current.OrganizationalUnits); + } + + [Fact] + public void RemovingRowsCanRepairForeignDomainDraft() + { + DomainJoinSettings draft = Catalog() with + { + DomainName = "other.test", + OrganizationalUnits = [Unit("manual", "Devices", "OU=Devices,DC=contoso,DC=test"), + Unit("server", "Servers", "OU=Servers,DC=contoso,DC=test")] + }; + DomainJoinSettings remaining = DomainJoinOrganizationalUnitCatalog.Remove(draft, "manual"); + Assert.Single(remaining.OrganizationalUnits); + Assert.Null(remaining.DefaultOuId); + DomainJoinSettings repaired = DomainJoinOrganizationalUnitCatalog.Remove(remaining, "server"); + Assert.Empty(repaired.OrganizationalUnits); + Assert.True(DomainJoinConfigurationValidator.ValidateMetadata(repaired).IsValid); + } + + [Fact] + public void SelectedImportEstablishesEmptyDomainAssociation() + { + DomainJoinSettings merged = DomainJoinOrganizationalUnitCatalog.Merge(new(), "CONTOSO.TEST.", + [Unit("first", "Devices", "OU=Devices,DC=contoso,DC=test")]); + Assert.Equal("contoso.test", merged.DomainName); + Assert.Equal("contoso.test", merged.OuCatalogDomain); + } + + [Fact] + public void MergeRejectsOverflowAndOutsideDomain() + { + Assert.Throws(() => DomainJoinOrganizationalUnitCatalog.Merge(new(), "contoso.test", + [Unit("foreign", "Foreign", "OU=Foreign,DC=other,DC=test")])); + var units = Enumerable.Range(0, 1025).Select(i => Unit(i.ToString(), "OU", $"OU=Unit{i},DC=contoso,DC=test")).ToArray(); + Assert.Throws(() => DomainJoinOrganizationalUnitCatalog.Merge(new(), "contoso.test", units)); + } + + private static DomainJoinSettings Catalog() => new() + { + DomainName = "contoso.test", + OuCatalogDomain = "contoso.test", + DefaultOuId = "manual", + AllowOuSelectionDuringDeployment = true, + OrganizationalUnits = [Unit("manual", "Manual label", "OU=Devices,DC=contoso,DC=test")] + }; + + private static DomainJoinOrganizationalUnitSettings Unit(string id, string label, string dn) => new() + { Id = id, DisplayName = label, DistinguishedName = dn }; +} diff --git a/src/Foundry.Core/Services/Configuration/DomainJoinOrganizationalUnitCatalog.cs b/src/Foundry.Core/Services/Configuration/DomainJoinOrganizationalUnitCatalog.cs new file mode 100644 index 000000000..249e62011 --- /dev/null +++ b/src/Foundry.Core/Services/Configuration/DomainJoinOrganizationalUnitCatalog.cs @@ -0,0 +1,70 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; + +namespace Foundry.Core.Services.Configuration; + +/// Maintains a bounded portable OU catalog without changing existing destination identities. +public static class DomainJoinOrganizationalUnitCatalog +{ + /// Merges selected discovered destinations into a validated copy. + public static DomainJoinSettings Merge(DomainJoinSettings current, string discoveredDomain, + IReadOnlyList selected) + { + ArgumentNullException.ThrowIfNull(current); + ArgumentNullException.ThrowIfNull(selected); + Validate(current); + if (!DomainJoinCredentialContext.IsValidDomainName(discoveredDomain)) + throw new ArgumentException("The discovered domain must be a DNS domain.", nameof(discoveredDomain)); + string domain = DomainJoinCredentialContext.CanonicalizeDomainName(discoveredDomain); + if (!Matches(current.DomainName) || !Matches(current.OuCatalogDomain)) + throw new ArgumentException("The target and catalog must match the discovered computer domain.", nameof(discoveredDomain)); + var units = current.OrganizationalUnits.ToList(); + var names = new HashSet(units.Select(unit => Key(unit.DistinguishedName)), StringComparer.OrdinalIgnoreCase); + foreach (DomainJoinOrganizationalUnitSettings unit in selected) + { + if (unit is null) throw new ArgumentException("A destination is required.", nameof(selected)); + if (names.Add(Key(unit.DistinguishedName))) units.Add(unit); + } + DomainJoinSettings result = current with + { + DomainName = string.IsNullOrWhiteSpace(current.DomainName) && selected.Count > 0 ? domain : current.DomainName, + OuCatalogDomain = units.Count > 0 ? domain : current.OuCatalogDomain, + OrganizationalUnits = units.ToArray() + }; + Validate(result); + return result; + + bool Matches(string? value) => string.IsNullOrWhiteSpace(value) || + DomainJoinCredentialContext.IsValidDomainName(value) && + DomainJoinCredentialContext.CanonicalizeDomainName(value) == domain; + } + + /// Removes a destination and clears its default selection, allowing inconsistent drafts to be repaired incrementally. + public static DomainJoinSettings Remove(DomainJoinSettings current, string id) + { + ArgumentNullException.ThrowIfNull(current); + DomainJoinOrganizationalUnitSettings[] units = current.OrganizationalUnits + .Where(unit => !string.Equals(unit.Id, id, StringComparison.OrdinalIgnoreCase)).ToArray(); + DomainJoinSettings result = current with + { + OrganizationalUnits = units, + OuCatalogDomain = units.Length == 0 ? null : current.OuCatalogDomain, + DefaultOuId = string.Equals(current.DefaultOuId, id, StringComparison.OrdinalIgnoreCase) ? null : current.DefaultOuId, + AllowOuSelectionDuringDeployment = units.Length > 0 && current.AllowOuSelectionDuringDeployment + }; + return result; + } + + private static string Key(string dn) => DistinguishedNameRules.TryParse(dn, out ParsedDistinguishedName parsed) + ? DistinguishedNameRules.GetComparisonKey(parsed) + : throw new ArgumentException("The destination distinguished name is invalid.", nameof(dn)); + + private static void Validate(DomainJoinSettings settings) + { + if (!DomainJoinConfigurationValidator.ValidateMetadata(settings).IsValid) + throw new ArgumentException("The destination catalog is invalid.", nameof(settings)); + } +} diff --git a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs index 12e7a84c2..a9e64fe14 100644 --- a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs @@ -172,6 +172,8 @@ public static IServiceCollection AddFoundryApplicationServices(this IServiceColl services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Foundry/Foundry.csproj b/src/Foundry/Foundry.csproj index 52ae776fd..b6ee0a5f2 100644 --- a/src/Foundry/Foundry.csproj +++ b/src/Foundry/Foundry.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -55,6 +55,7 @@ + diff --git a/src/Foundry/FoundryApplicationInfo.cs b/src/Foundry/FoundryApplicationInfo.cs index 22f94346b..7b5f5f031 100644 --- a/src/Foundry/FoundryApplicationInfo.cs +++ b/src/Foundry/FoundryApplicationInfo.cs @@ -48,6 +48,12 @@ public static class FoundryApplicationInfo /// public const string AutopilotInteractiveDocumentationUrl = DocumentationUrl + "/foundry-osd/autopilot/interactive-hardware-hash"; + /// Gets the interactive domain joining documentation URL. + public const string InteractiveDomainJoinDocumentationUrl = DocumentationUrl + "/foundry-osd/domain-join/interactive"; + + /// Gets the automatic domain joining documentation URL. + public const string ZeroTouchDomainJoinDocumentationUrl = DocumentationUrl + "/foundry-osd/domain-join/zero-touch"; + /// /// Gets the general configuration documentation URL. /// diff --git a/src/Foundry/Services/Configuration/ConfigurationOverviewService.cs b/src/Foundry/Services/Configuration/ConfigurationOverviewService.cs index af764d604..37370797e 100644 --- a/src/Foundry/Services/Configuration/ConfigurationOverviewService.cs +++ b/src/Foundry/Services/Configuration/ConfigurationOverviewService.cs @@ -18,6 +18,7 @@ internal sealed class ConfigurationOverviewService : IConfigurationOverviewServi private readonly IDeploymentProtectionSecretStateService deploymentProtectionSecretStateService; private readonly INetworkSecretStateService networkSecretStateService; private readonly IOobeAccountSecretStateService oobeAccountSecretStateService; + private readonly IDomainJoinSecretStateService domainJoinSecretStateService; private readonly IWinPeLanguageDiscoveryService languageDiscoveryService; private ConfigurationOverviewEvaluation? cachedEvaluation; @@ -27,6 +28,7 @@ public ConfigurationOverviewService( IDeploymentProtectionSecretStateService deploymentProtectionSecretStateService, INetworkSecretStateService networkSecretStateService, IOobeAccountSecretStateService oobeAccountSecretStateService, + IDomainJoinSecretStateService domainJoinSecretStateService, IWinPeLanguageDiscoveryService languageDiscoveryService) { this.adkService = adkService; @@ -34,6 +36,7 @@ public ConfigurationOverviewService( this.deploymentProtectionSecretStateService = deploymentProtectionSecretStateService; this.networkSecretStateService = networkSecretStateService; this.oobeAccountSecretStateService = oobeAccountSecretStateService; + this.domainJoinSecretStateService = domainJoinSecretStateService; this.languageDiscoveryService = languageDiscoveryService; adkService.StatusChanged += OnAdkStatusChanged; @@ -41,6 +44,7 @@ public ConfigurationOverviewService( deploymentProtectionSecretStateService.Changed += OnUnderlyingStateChanged; networkSecretStateService.Changed += OnUnderlyingStateChanged; oobeAccountSecretStateService.Changed += OnUnderlyingStateChanged; + domainJoinSecretStateService.Changed += OnUnderlyingStateChanged; } public event EventHandler? Changed; @@ -65,6 +69,7 @@ public ConfigurationOverviewEvaluation Evaluate() deploymentProtectionSecretStateService.IsValid, IsOobeAccountConfigurationReady = oobeAccountSecretStateService.Validate(configuration.Customization.Oobe).IsValid, IsAutopilotConfigurationReady = configurationStateService.IsAutopilotConfigurationReady, + IsDomainJoinConfigurationReady = configurationStateService.IsDomainJoinConfigurationReady, IsCustomImagesReady = configurationStateService.IsCustomImagesReady, IsPostInstallationReady = configurationStateService.IsPostInstallationReady, IsUnattendConfigurationReady = configurationStateService.IsUnattendConfigurationReady diff --git a/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs b/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs index 1d6264f7d..faccdfa75 100644 --- a/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs +++ b/src/Foundry/Services/Configuration/FoundryConfigurationStateService.cs @@ -191,6 +191,12 @@ public bool IsDeployConfigurationReady /// public bool IsAutopilotConfigurationReady => AutopilotConfigurationValidation.IsReady; + /// + public bool IsDomainJoinConfigurationReady => DomainJoinConfigurationValidator.EvaluateReadiness( + Current.DomainJoin, + domainJoinSecretStateService.HasPassword(new(Current.DomainJoin.DomainName ?? string.Empty, Current.DomainJoin.AccountName ?? string.Empty)), + Current.General.DeploymentProtection.IsEnabled && deploymentProtectionSecretStateService.IsValid).IsValid; + /// public bool IsUnattendConfigurationReady { @@ -368,9 +374,20 @@ public void UpdateCustomization(CustomizationSettings settings) /// public void UpdateAutopilot(AutopilotSettings settings) { - ArgumentNullException.ThrowIfNull(settings); - Current = Current with { Autopilot = SanitizeAutopilotForPersistence(settings) }; - Save(); + UpdateProvisioningSelection(settings, Current.DomainJoin); + } + + /// + public void UpdateDomainJoin(DomainJoinSettings settings) => UpdateProvisioningSelection(Current.Autopilot, settings); + + /// + public void UpdateProvisioningSelection(AutopilotSettings autopilot, DomainJoinSettings domainJoin) + { + DomainJoinConfigurationValidator.ThrowIfProvisioningModesConflict(autopilot, domainJoin); + FoundryConfigurationDocument candidate = Current with { Autopilot = SanitizeAutopilotForPersistence(autopilot), DomainJoin = domainJoin }; + Save(candidate, throwOnFailure: true); + Current = candidate; + domainJoinSecretStateService.Update(domainJoin); StateChanged?.Invoke(this, EventArgs.Empty); } @@ -657,6 +674,7 @@ private void Save(FoundryConfigurationDocument? candidate = null, bool throwOnFa private static FoundryConfigurationDocument SanitizeForPersistence(FoundryConfigurationDocument document) { + DomainJoinConfigurationValidator.ThrowIfProvisioningModesConflict(document.Autopilot, document.DomainJoin); return document with { General = SanitizeGeneralForPersistence(document.General), diff --git a/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs b/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs index 7158aeb7b..8e23d479b 100644 --- a/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs +++ b/src/Foundry/Services/Configuration/IFoundryConfigurationStateService.cs @@ -141,6 +141,15 @@ public interface IFoundryConfigurationStateService /// New Autopilot settings. void UpdateAutopilot(AutopilotSettings settings); + /// Persists domain metadata and clears credentials whose ownership is no longer active. + void UpdateDomainJoin(DomainJoinSettings settings); + + /// Publishes one exclusive provisioning change after a successful candidate save. + void UpdateProvisioningSelection(AutopilotSettings autopilot, DomainJoinSettings domainJoin); + + /// Gets domain metadata and automatic credential/protection readiness for the active mode. + bool IsDomainJoinConfigurationReady { get; } + /// /// Persists answer-file metadata and source references without storing XML content. /// diff --git a/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs b/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs new file mode 100644 index 000000000..ce116ead4 --- /dev/null +++ b/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs @@ -0,0 +1,219 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.DirectoryServices.Protocols; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; +using Serilog; + +namespace Foundry.Services.DomainJoin; + +/// Owns serialized native locator/bind work until it drains, including after the UI stops waiting. +internal sealed class AuthoringDomainOuDiscoveryService : IAuthoringDomainOuDiscoveryService +{ + private readonly SemaphoreSlim operationGate = new(1, 1); + private readonly ILogger logger; + private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(10); + + public AuthoringDomainOuDiscoveryService(ILogger logger) => this.logger = logger.ForContext(); + + /// + public async Task DiscoverAsync(CancellationToken cancellationToken) + { + logger.Information("Domain destination discovery started."); + var lifetime = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + lifetime.CancelAfter(TimeSpan.FromSeconds(60)); + Task work = Task.Run(async () => + { + DomainOuDiscoveryResult? result = null; + try + { + await operationGate.WaitAsync(lifetime.Token).ConfigureAwait(false); + try + { + result = await DiscoverOwnedAsync(lifetime.Token).ConfigureAwait(false); + if (result.Status == DomainOuDiscoveryStatus.Canceled && !cancellationToken.IsCancellationRequested) + result = result with { Status = DomainOuDiscoveryStatus.Unavailable, ErrorCode = "Timeout" }; + return result; + } + finally { operationGate.Release(); } + } + catch (OperationCanceledException) + { + return result = new(null, null, [], cancellationToken.IsCancellationRequested ? DomainOuDiscoveryStatus.Canceled : DomainOuDiscoveryStatus.Unavailable, + cancellationToken.IsCancellationRequested ? "Canceled" : "Timeout"); + } + finally + { + lifetime.Dispose(); + logger.Information("Domain destination discovery ended. Status={Status}, CandidateCount={CandidateCount}, FailureCode={FailureCode}, NativeErrorCode={NativeErrorCode}, LdapErrorCode={LdapErrorCode}, DirectoryResultCode={DirectoryResultCode}.", + result?.Status.ToString() ?? "Failed", result?.Candidates.Count ?? 0, result?.ErrorCode, + result?.NativeErrorCode, result?.LdapErrorCode, result?.DirectoryResultCode); + } + }, CancellationToken.None); + try { return await work.WaitAsync(TimeSpan.FromSeconds(60), cancellationToken).ConfigureAwait(false); } + catch (TimeoutException) { return new(null, null, [], DomainOuDiscoveryStatus.Unavailable, "Timeout"); } + catch (OperationCanceledException) { return new(null, null, [], DomainOuDiscoveryStatus.Canceled, "Canceled"); } + } + + private static async Task DiscoverOwnedAsync(CancellationToken token) + { + string? domain = null; + string? namingContext = null; + List candidates = []; + try + { + token.ThrowIfCancellationRequested(); + (domain, string controller) = LocateComputerDomain(); + token.ThrowIfCancellationRequested(); + using var connection = new LdapConnection(new LdapDirectoryIdentifier(controller, 389, true, false), null, AuthType.Negotiate); + connection.Timeout = RequestTimeout; + connection.SessionOptions.ProtocolVersion = 3; + connection.SessionOptions.Signing = true; + connection.SessionOptions.Sealing = true; + connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None; + connection.SessionOptions.AutoReconnect = false; + connection.SessionOptions.SendTimeout = RequestTimeout; + connection.Bind(); + token.ThrowIfCancellationRequested(); + var rootRequest = new SearchRequest("", "(objectClass=*)", SearchScope.Base, "defaultNamingContext", "dnsHostName", "supportedControl") + { TimeLimit = RequestTimeout }; + var root = (SearchResponse)await SendOwnedAsync(connection, rootRequest, token).ConfigureAwait(false); + if (root.Entries.Count != 1 || root.References.Count > 0) throw new InvalidDataException(); + namingContext = Attribute(root.Entries[0], "defaultNamingContext"); + if (!DistinguishedNameRules.IsWithinDomain(namingContext, domain)) throw new InvalidDataException(); + var request = new SearchRequest(namingContext, "(objectCategory=organizationalUnit)", SearchScope.Subtree, "distinguishedName", "name", "objectGUID") + { TimeLimit = RequestTimeout }; + var paging = new PageResultRequestControl(256) { IsCritical = true }; + request.Controls.Add(paging); + request.Controls.Add(new DomainScopeControl { IsCritical = true }); + while (true) + { + var response = (SearchResponse)await SendOwnedAsync(connection, request, token).ConfigureAwait(false); + bool valid = AddEntries(response, candidates, domain); + PageResultResponseControl[] pages = response.Controls.OfType().ToArray(); + if (!valid || response.References.Count > 0 || pages.Length != 1) + return new(domain, namingContext, candidates, DomainOuDiscoveryStatus.Incomplete, "Incomplete"); + if (pages[0].Cookie.Length == 0) + return new(domain, namingContext, candidates, DomainOuDiscoveryStatus.Complete); + if (candidates.Count >= 4096) + return new(domain, namingContext, candidates, DomainOuDiscoveryStatus.Incomplete, "Limit"); + paging.Cookie = pages[0].Cookie; + } + } + catch (OperationCanceledException) { return new(domain, namingContext, [], DomainOuDiscoveryStatus.Canceled, "Canceled"); } + catch (DirectoryOperationException ex) + { + if (ex.Response is SearchResponse partial && domain is not null) AddEntries(partial, candidates, domain); + bool incomplete = candidates.Count > 0 || ex.Response?.ResultCode is ResultCode.TimeLimitExceeded or ResultCode.SizeLimitExceeded or ResultCode.Referral; + return new(domain, namingContext, candidates, incomplete ? DomainOuDiscoveryStatus.Incomplete : DomainOuDiscoveryStatus.Unavailable, + "DirectoryRead", DirectoryResultCode: ex.Response is null ? null : (int)ex.Response.ResultCode); + } + catch (Win32Exception ex) { return new(domain, namingContext, [], DomainOuDiscoveryStatus.Unavailable, "ComputerDomainUnavailable", NativeErrorCode: ex.NativeErrorCode); } + catch (Exception ex) when (ex is LdapException or InvalidDataException or InvalidOperationException or ArgumentException or NotSupportedException) + { + return new(domain, namingContext, candidates, candidates.Count > 0 ? DomainOuDiscoveryStatus.Incomplete : DomainOuDiscoveryStatus.Unavailable, + ex is InvalidOperationException ? "ComputerDomainUnavailable" : "DirectoryRead", LdapErrorCode: (ex as LdapException)?.ErrorCode); + } + } + + private static bool AddEntries(SearchResponse response, List candidates, string domain) + { + bool valid = true; + foreach (SearchResultEntry entry in response.Entries) + { + if (candidates.Count >= 4096) return false; + string dn = Attribute(entry, "distinguishedName"); + string name = Attribute(entry, "name"); + DirectoryAttribute? guidAttribute = entry.Attributes["objectGUID"]; + if (!DistinguishedNameRules.IsWithinDomain(dn, domain) || name.Length is 0 or > 120 || name.Contains('\0') || + guidAttribute?.Count != 1 || guidAttribute[0] is not byte[] { Length: 16 } guid) + { valid = false; continue; } + candidates.Add(new() { Id = new Guid(guid).ToString("D"), DisplayName = name, DistinguishedName = dn }); + } + return valid; + } + + private static string Attribute(SearchResultEntry entry, string name) => entry.Attributes[name] is { Count: 1 } attribute && attribute[0] is string value + ? value : string.Empty; + + private static async Task SendOwnedAsync(LdapConnection connection, DirectoryRequest request, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var sync = new object(); + bool settled = false; + // Begin can synchronously connect/send. Cancellation is observed after it returns; the worker retains ownership. + IAsyncResult pending = connection.BeginSendRequest(request, RequestTimeout, PartialResultProcessing.NoPartialResultSupport, result => + { + lock (sync) + { + if (settled) return; + settled = true; + try { completion.TrySetResult(connection.EndSendRequest(result)); } + catch (Exception ex) { completion.TrySetException(ex); } + } + }, null); + using CancellationTokenRegistration registration = token.Register(() => + { + lock (sync) + { + if (settled) return; + settled = true; + try { connection.Abort(pending); } + catch (ArgumentException) { } + finally { completion.TrySetCanceled(token); } + } + }); + return await completion.Task.ConfigureAwait(false); + } + + private static (string Domain, string Controller) LocateComputerDomain() + { + nint membership = 0; + nint locator = 0; + try + { + uint status = NetGetJoinInformation(null, out membership, out int joinStatus); + if (status != 0) throw new Win32Exception(unchecked((int)status)); + if (joinStatus != 3) throw new InvalidOperationException(); + string flatDomain = Marshal.PtrToStringUni(membership) ?? throw new InvalidDataException(); + status = DsGetDcName(null, flatDomain, 0, null, 0x10 | 0x10000 | 0x40000000, out locator); + if (status != 0) throw new Win32Exception(unchecked((int)status)); + DomainControllerInfo info = Marshal.PtrToStructure(locator); + string domain = Marshal.PtrToStringUni(info.DomainName) ?? string.Empty; + string controller = (Marshal.PtrToStringUni(info.DomainControllerName) ?? string.Empty).TrimStart('\\'); + if (!DomainJoinCredentialContext.IsValidDomainName(domain) || !DomainJoinCredentialContext.IsValidDomainName(controller)) throw new InvalidDataException(); + return (DomainJoinCredentialContext.CanonicalizeDomainName(domain), controller); + } + finally + { + if (locator != 0) NetApiBufferFree(locator); + if (membership != 0) NetApiBufferFree(membership); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct DomainControllerInfo + { + public nint DomainControllerName; + public nint DomainControllerAddress; + public uint DomainControllerAddressType; + public Guid DomainGuid; + public nint DomainName; + public nint DnsForestName; + public uint Flags; + public nint DcSiteName; + public nint ClientSiteName; + } + + [DllImport("netapi32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] + private static extern uint NetGetJoinInformation(string? server, out nint nameBuffer, out int status); + [DllImport("netapi32.dll", EntryPoint = "DsGetDcNameW", ExactSpelling = true, CharSet = CharSet.Unicode)] + private static extern uint DsGetDcName(string? computer, string? domain, nint guid, string? site, uint flags, out nint info); + [DllImport("netapi32.dll", ExactSpelling = true)] + private static extern uint NetApiBufferFree(nint buffer); +} diff --git a/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs b/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs new file mode 100644 index 000000000..0c3da9aa2 --- /dev/null +++ b/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs @@ -0,0 +1,22 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; + +namespace Foundry.Services.DomainJoin; + +/// Discovers destinations from the authoring computer's AD domain using its current Windows identity. +public interface IAuthoringDomainOuDiscoveryService +{ + /// Returns a bounded preview without retaining an authenticated connection. + Task DiscoverAsync(CancellationToken cancellationToken); +} + +/// Distinguishes a complete catalog preview from partial results and unavailable discovery. +public enum DomainOuDiscoveryStatus { Complete, Incomplete, Unavailable, Canceled } + +/// Contains only portable directory metadata and an allowlisted presentation code. +public sealed record DomainOuDiscoveryResult(string? ComputerDomain, string? NamingContext, + IReadOnlyList Candidates, DomainOuDiscoveryStatus Status, string? ErrorCode = null, + int? NativeErrorCode = null, int? LdapErrorCode = null, int? DirectoryResultCode = null); diff --git a/src/Foundry/Services/Shell/NavigationRouteCatalog.cs b/src/Foundry/Services/Shell/NavigationRouteCatalog.cs index 8f0f09f18..31df14e2b 100644 --- a/src/Foundry/Services/Shell/NavigationRouteCatalog.cs +++ b/src/Foundry/Services/Shell/NavigationRouteCatalog.cs @@ -11,7 +11,8 @@ public enum NavigationSection General, Network, WindowsAutopilot, - Customization + Customization, + DomainJoin } public sealed record NavigationRoute( @@ -38,6 +39,8 @@ public static class NavigationRouteCatalog CreatePrimary("Nav_AutopilotJsonProfileKey", "E8A5", NavigationSection.WindowsAutopilot), CreatePrimary("Nav_AutopilotZeroTouchKey", "E753", NavigationSection.WindowsAutopilot), CreatePrimary("Nav_AutopilotInteractiveHashUploadKey", "E928", NavigationSection.WindowsAutopilot), + CreatePrimary("Nav_InteractiveDomainJoinKey", "E902", NavigationSection.DomainJoin), + CreatePrimary("Nav_ZeroTouchDomainJoinKey", "E902", NavigationSection.DomainJoin), CreatePrimary("Nav_OsSelectionKey", "EC77", NavigationSection.Customization), CreatePrimary("Nav_CustomImagesKey", "E958", NavigationSection.Customization), CreatePrimary("Nav_UnattendKey", "E8A5", NavigationSection.Customization), @@ -71,6 +74,7 @@ public static class NavigationRouteCatalog NavigationSection.General => "Nav_GeneralSection.Title", NavigationSection.Network => "Nav_NetworkSection.Title", NavigationSection.WindowsAutopilot => "Nav_WindowsAutopilotSection.Title", + NavigationSection.DomainJoin => "Nav_DomainJoinSection.Title", NavigationSection.Customization => "Nav_CustomizationSection.Title", _ => throw new ArgumentOutOfRangeException(nameof(section), section, null) }; diff --git a/src/Foundry/Services/Shell/NavigationStatusService.cs b/src/Foundry/Services/Shell/NavigationStatusService.cs index b97d4fe72..028542f26 100644 --- a/src/Foundry/Services/Shell/NavigationStatusService.cs +++ b/src/Foundry/Services/Shell/NavigationStatusService.cs @@ -49,12 +49,15 @@ public NavigationStatusService( bool isAutopilotTarget = target is ConfigurationNavigationTarget.AutopilotJsonProfile or ConfigurationNavigationTarget.AutopilotHardwareHashUpload or - ConfigurationNavigationTarget.AutopilotInteractiveHardwareHashUpload; + ConfigurationNavigationTarget.AutopilotInteractiveHardwareHashUpload or + ConfigurationNavigationTarget.DomainJoinInteractive or ConfigurationNavigationTarget.DomainJoinAutomatic; return ToNavigationStatus(state, isAutopilotTarget); } private static ConfigurationNavigationTarget ResolveTarget(Type pageType) { + if (pageType == typeof(InteractiveDomainJoinPage)) return ConfigurationNavigationTarget.DomainJoinInteractive; + if (pageType == typeof(ZeroTouchDomainJoinPage)) return ConfigurationNavigationTarget.DomainJoinAutomatic; if (pageType == typeof(GeneralConfigurationPage)) { return ConfigurationNavigationTarget.General; diff --git a/src/Foundry/Strings/ar-SA/Resources.resw b/src/Foundry/Strings/ar-SA/Resources.resw index e1227d53e..e5dda9cf3 100644 --- a/src/Foundry/Strings/ar-SA/Resources.resw +++ b/src/Foundry/Strings/ar-SA/Resources.resw @@ -1828,6 +1828,183 @@ لا يمكن فتح الوثائق + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + ملف شهادة CA الجذر الموثوق به @@ -1981,6 +2158,12 @@ حالة جاهزية الوسائط + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + العربية (المملكة العربية السعودية) @@ -2191,6 +2374,9 @@ التوثيق + + Domain Join + يتوفر الاتصال القياسي Ethernet بشكل افتراضي. قم بتكوين هذا القسم فقط إذا كانت شبكتك السلكية تتطلب مصادقة مؤسسة 802.1X. @@ -2212,6 +2398,12 @@ الصفحة الرئيسية + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + قم بتكوين أسماء الأجهزة المنشورة. @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + نمط التهيئة النشط @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/bg-BG/Resources.resw b/src/Foundry/Strings/bg-BG/Resources.resw index daf0c6e46..25d724149 100644 --- a/src/Foundry/Strings/bg-BG/Resources.resw +++ b/src/Foundry/Strings/bg-BG/Resources.resw @@ -1828,6 +1828,183 @@ Документацията не можа да бъде отворена + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Файл със сертификат за доверен основен CA @@ -1981,6 +2158,12 @@ Състояние на готовност на носителя + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + арабски (Саудитска Арабия) @@ -2191,6 +2374,9 @@ Документация + + Domain Join + Стандартната Ethernet връзка е налична по подразбиране. Конфигурирайте този раздел само ако вашата кабелна мрежа изисква 802.1X корпоративно удостоверяване. @@ -2212,6 +2398,12 @@ Начало + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Конфигурирайте имената на внедрените устройства. @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Активен режим на зaсигуряване @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/cs-CZ/Resources.resw b/src/Foundry/Strings/cs-CZ/Resources.resw index 7a8f402d8..27d21edf7 100644 --- a/src/Foundry/Strings/cs-CZ/Resources.resw +++ b/src/Foundry/Strings/cs-CZ/Resources.resw @@ -1828,6 +1828,183 @@ Licencováno pod licencí MIT. Dokumentaci nelze otevřít + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Soubor certifikátu důvěryhodného kořenového CA @@ -1981,6 +2158,12 @@ Licencováno pod licencí MIT. Stav připravenosti média + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabština (Saúdská Arábie) @@ -2191,6 +2374,9 @@ Licencováno pod licencí MIT. Dokumentace + + Domain Join + Ve výchozím nastavení je k dispozici standardní připojení Ethernet. Tuto část nakonfigurujte pouze v případě, že vaše kabelová síť vyžaduje podnikové ověřování 802.1X. @@ -2212,6 +2398,12 @@ Licencováno pod licencí MIT. Domů + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurujte názvy nasazených zařízení. @@ -2272,6 +2464,12 @@ Licencováno pod licencí MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktivní režim zřizování @@ -3902,4 +4100,10 @@ Použití aktualizace restartuje Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/da-DK/Resources.resw b/src/Foundry/Strings/da-DK/Resources.resw index cbe6ac806..4ee6fed6e 100644 --- a/src/Foundry/Strings/da-DK/Resources.resw +++ b/src/Foundry/Strings/da-DK/Resources.resw @@ -1828,6 +1828,183 @@ Licenseret under MIT-licensen. Dokumentation kunne ikke åbnes + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Betroet rod-CA-certifikatfil @@ -1981,6 +2158,12 @@ Licenseret under MIT-licensen. Status for medieberedskab + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabisk (Saudi-Arabien) @@ -2191,6 +2374,9 @@ Licenseret under MIT-licensen. Dokumentation + + Domain Join + Standard Ethernet-forbindelse er tilgængelig som standard. Konfigurer kun denne sektion, hvis dit kablede netværk kræver 802.1X virksomhedsgodkendelse. @@ -2212,6 +2398,12 @@ Licenseret under MIT-licensen. Hjem + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurer deployerede enhedsnavne. @@ -2272,6 +2464,12 @@ Licenseret under MIT-licensen. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktiv provisioning-tilstand @@ -3902,4 +4100,10 @@ Anvendelse af opdateringen genstarter Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/de-DE/Resources.resw b/src/Foundry/Strings/de-DE/Resources.resw index 76fabc1f5..34eba195a 100644 --- a/src/Foundry/Strings/de-DE/Resources.resw +++ b/src/Foundry/Strings/de-DE/Resources.resw @@ -1828,6 +1828,183 @@ Lizenziert unter der MIT-Lizenz. Dokumentation konnte nicht geöffnet werden + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Vertrauenswürdige Root-CA-Zertifikatdatei @@ -1981,6 +2158,12 @@ Lizenziert unter der MIT-Lizenz. Status der Medienbereitschaft + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabisch (Saudi-Arabien) @@ -2191,6 +2374,9 @@ Lizenziert unter der MIT-Lizenz. Dokumentation + + Domain Join + Die standardmäßige Ethernet-Konnektivität ist standardmäßig verfügbar. Konfigurieren Sie diesen Abschnitt nur, wenn Ihr kabelgebundenes Netzwerk eine 802.1X-Unternehmensauthentifizierung erfordert. @@ -2212,6 +2398,12 @@ Lizenziert unter der MIT-Lizenz. Zuhause + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurieren Sie die Namen bereitgestellter Geräte. @@ -2272,6 +2464,12 @@ Lizenziert unter der MIT-Lizenz. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktiverter Bereitstellungsmodus @@ -3902,4 +4100,10 @@ Das Anwenden des Updates startet Foundry OSD neu. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/el-GR/Resources.resw b/src/Foundry/Strings/el-GR/Resources.resw index c6f5fec39..9d56de23c 100644 --- a/src/Foundry/Strings/el-GR/Resources.resw +++ b/src/Foundry/Strings/el-GR/Resources.resw @@ -1828,6 +1828,183 @@ Δεν ήταν δυνατό το άνοιγμα της τεκμηρίωσης + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Αξιόπιστο αρχείο πιστοποιητικού CA root @@ -1981,6 +2158,12 @@ Κατάσταση ετοιμότητας μέσου + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Αραβικά (Σαουδική Αραβία) @@ -2191,6 +2374,9 @@ Τεκμηρίωση + + Domain Join + Η τυπική συνδεσιμότητα Ethernet είναι διαθέσιμη από προεπιλογή. Διαμορφώστε αυτήν την ενότητα μόνο εάν το ενσύρματο δίκτυό σας απαιτεί εταιρικό έλεγχο ταυτότητας 802.1X. @@ -2212,6 +2398,12 @@ Σπίτι + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Διαμορφώστε τα ονόματα των αναπτυγμένων συσκευών. @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Ενεργή λειτουργία προμήθειας @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/en-GB/Resources.resw b/src/Foundry/Strings/en-GB/Resources.resw index 8aa82bd90..8aa990077 100644 --- a/src/Foundry/Strings/en-GB/Resources.resw +++ b/src/Foundry/Strings/en-GB/Resources.resw @@ -1828,6 +1828,183 @@ Licensed under the MIT License. Documentation could not be opened + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Trusted root CA certificate file @@ -1981,6 +2158,12 @@ Licensed under the MIT License. Media readiness status + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabic (Saudi Arabia) @@ -2191,6 +2374,9 @@ Licensed under the MIT License. Documentation + + Domain Join + Standard Ethernet connectivity is available by default. Configure this section only if your wired network requires 802.1X enterprise authentication. @@ -2212,6 +2398,12 @@ Licensed under the MIT License. Home + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configure deployed device names. @@ -2272,6 +2464,12 @@ Licensed under the MIT License. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Active provisioning mode @@ -3902,4 +4100,10 @@ Applying the update restarts Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/en-US/Resources.resw b/src/Foundry/Strings/en-US/Resources.resw index 86a8c3c35..57586642c 100644 --- a/src/Foundry/Strings/en-US/Resources.resw +++ b/src/Foundry/Strings/en-US/Resources.resw @@ -1828,6 +1828,183 @@ Licensed under the MIT License. Documentation could not be opened + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Trusted root CA certificate file @@ -1981,6 +2158,12 @@ Licensed under the MIT License. Media readiness status + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabic (Saudi Arabia) @@ -2191,6 +2374,9 @@ Licensed under the MIT License. Documentation + + Domain Join + Configure wired 802.1X authentication. @@ -2212,6 +2398,12 @@ Licensed under the MIT License. Home + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configure deployed device names. @@ -2272,6 +2464,12 @@ Licensed under the MIT License. Windows Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Active provisioning mode @@ -3902,4 +4100,10 @@ Applying the update restarts Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/es-ES/Resources.resw b/src/Foundry/Strings/es-ES/Resources.resw index 76e2eb43d..b12010e2f 100644 --- a/src/Foundry/Strings/es-ES/Resources.resw +++ b/src/Foundry/Strings/es-ES/Resources.resw @@ -1828,6 +1828,183 @@ Licenciado bajo la Licencia MIT. No se pudo abrir la documentación + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Archivo de certificado de CA raíz de confianza @@ -1981,6 +2158,12 @@ Licenciado bajo la Licencia MIT. Estado de preparación del medio + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Árabe (Arabia Saudita) @@ -2191,6 +2374,9 @@ Licenciado bajo la Licencia MIT. Documentación + + Domain Join + La conectividad estándar Ethernet está disponible de forma predeterminada. Configure esta sección solo si su red cableada requiere autenticación empresarial 802.1X. @@ -2212,6 +2398,12 @@ Licenciado bajo la Licencia MIT. Hogar + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configure los nombres de dispositivos implementados. @@ -2272,6 +2464,12 @@ Licenciado bajo la Licencia MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Modo de aprovisionamiento activo @@ -3902,4 +4100,10 @@ Al aplicar la actualización, Foundry OSD se reiniciará. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/es-MX/Resources.resw b/src/Foundry/Strings/es-MX/Resources.resw index 02e4537ef..6672b16a6 100644 --- a/src/Foundry/Strings/es-MX/Resources.resw +++ b/src/Foundry/Strings/es-MX/Resources.resw @@ -1828,6 +1828,183 @@ Licenciado bajo la Licencia MIT. No se pudo abrir la documentación + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Archivo de certificado de CA raíz de confianza @@ -1981,6 +2158,12 @@ Licenciado bajo la Licencia MIT. Estado de preparación del medio + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Árabe (Arabia Saudita) @@ -2191,6 +2374,9 @@ Licenciado bajo la Licencia MIT. Documentación + + Domain Join + La conectividad estándar Ethernet está disponible de forma predeterminada. Configure esta sección solo si su red cableada requiere autenticación empresarial 802.1X. @@ -2212,6 +2398,12 @@ Licenciado bajo la Licencia MIT. Hogar + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configure los nombres de dispositivos implementados. @@ -2272,6 +2464,12 @@ Licenciado bajo la Licencia MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Modo de aprovisionamiento activo @@ -3902,4 +4100,10 @@ Al aplicar la actualización, Foundry OSD se reiniciará. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/et-EE/Resources.resw b/src/Foundry/Strings/et-EE/Resources.resw index ebd64cf72..900cbc51b 100644 --- a/src/Foundry/Strings/et-EE/Resources.resw +++ b/src/Foundry/Strings/et-EE/Resources.resw @@ -1828,6 +1828,183 @@ Litsentsitud MIT-i litsentsi alusel. Dokumentatsiooni ei saanud avada + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Usaldusväärne juur-CA sertifikaadi fail @@ -1981,6 +2158,12 @@ Litsentsitud MIT-i litsentsi alusel. Andmekandja valmisoleku olek + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + araabia (Saudi Araabia) @@ -2191,6 +2374,9 @@ Litsentsitud MIT-i litsentsi alusel. Dokumentatsioon + + Domain Join + Standardne Ethernet ühenduvus on vaikimisi saadaval. Seadistage see jaotis ainult siis, kui teie traadiga võrk nõuab 802.1X ettevõtte autentimist. @@ -2212,6 +2398,12 @@ Litsentsitud MIT-i litsentsi alusel. Kodu + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Seadistage juurutatud seadmete nimed. @@ -2272,6 +2464,12 @@ Litsentsitud MIT-i litsentsi alusel. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktiivne juurutusrežiim @@ -3902,4 +4100,10 @@ Värskenduse rakendamine taaskäivitab Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/fi-FI/Resources.resw b/src/Foundry/Strings/fi-FI/Resources.resw index 28f45b7a6..71b9c214e 100644 --- a/src/Foundry/Strings/fi-FI/Resources.resw +++ b/src/Foundry/Strings/fi-FI/Resources.resw @@ -1828,6 +1828,183 @@ Lisensoitu MIT-lisenssillä. Asiakirjoja ei voitu avata + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Luotettu juurivarmennetiedosto @@ -1981,6 +2158,12 @@ Lisensoitu MIT-lisenssillä. Tietovälineen valmiustila + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabia (Saudi-Arabia) @@ -2191,6 +2374,9 @@ Lisensoitu MIT-lisenssillä. Dokumentaatio + + Domain Join + Normaali Ethernet-yhteys on käytettävissä oletuksena. Määritä tämä osio vain, jos kiinteä verkkosi vaatii 802.1X yritystodennusta. @@ -2212,6 +2398,12 @@ Lisensoitu MIT-lisenssillä. Kotiin + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Määritä käyttöön otettujen laitteiden nimet. @@ -2272,6 +2464,12 @@ Lisensoitu MIT-lisenssillä. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktiivinen käyttöönotto-tila @@ -3902,4 +4100,10 @@ Päivityksen asentaminen käynnistää Foundry OSD:n uudelleen. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/fr-CA/Resources.resw b/src/Foundry/Strings/fr-CA/Resources.resw index 92cffdd35..1ad6f6cd0 100644 --- a/src/Foundry/Strings/fr-CA/Resources.resw +++ b/src/Foundry/Strings/fr-CA/Resources.resw @@ -1828,6 +1828,183 @@ Sous licence MIT. La documentation n'a pas pu être ouverte + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Fichier de certificat d'autorité de certification racine de confiance @@ -1981,6 +2158,12 @@ Sous licence MIT. État de préparation du média + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabe (Arabie Saoudite) @@ -2191,6 +2374,9 @@ Sous licence MIT. Documentation + + Domain Join + La connectivité standard Ethernet est disponible par défaut. Configurez cette section uniquement si votre réseau câblé nécessite une authentification d'entreprise 802.1X. @@ -2212,6 +2398,12 @@ Sous licence MIT. Accueil + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configurer les noms d'appareil déployés. @@ -2272,6 +2464,12 @@ Sous licence MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Mode de provisionnement actif @@ -3902,4 +4100,10 @@ L’application de la mise à jour redémarre Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/fr-FR/Resources.resw b/src/Foundry/Strings/fr-FR/Resources.resw index 27e69eafc..d3972ec56 100644 --- a/src/Foundry/Strings/fr-FR/Resources.resw +++ b/src/Foundry/Strings/fr-FR/Resources.resw @@ -1828,6 +1828,183 @@ Sous licence MIT. Impossible d’ouvrir la documentation + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Fichier de certificat d'autorité racine de confiance @@ -1981,6 +2158,12 @@ Sous licence MIT. État de préparation du média + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabe (Arabie Saoudite) @@ -2191,6 +2374,9 @@ Sous licence MIT. Documentation + + Domain Join + La connectivité Ethernet standard est disponible par défaut. Configurez cette section uniquement si votre réseau filaire nécessite une authentification 802.1X. @@ -2212,6 +2398,12 @@ Sous licence MIT. Accueil + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configurer les noms d’appareil déployés. @@ -2272,6 +2464,12 @@ Sous licence MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Mode de provisionnement actif @@ -3902,4 +4100,10 @@ L’application de la mise à jour redémarre Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/he-IL/Resources.resw b/src/Foundry/Strings/he-IL/Resources.resw index 54174beef..e0eadc5b9 100644 --- a/src/Foundry/Strings/he-IL/Resources.resw +++ b/src/Foundry/Strings/he-IL/Resources.resw @@ -1828,6 +1828,183 @@ לא ניתן היה לפתוח את התיעוד + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + קובץ אישור CA שורש מהימן @@ -1981,6 +2158,12 @@ מצב מוכנות המדיה + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + ערבית (ערב הסעודית) @@ -2191,6 +2374,9 @@ תיעוד + + Domain Join + קישוריות Ethernet סטנדרטית זמינה כברירת מחדל. הגדר את הסעיף הזה רק אם הרשת הקווית שלך דורשת אימות ארגוני 802.1X. @@ -2212,6 +2398,12 @@ בית + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + הגדר שמות למכשירים שנפרסו. @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + מצב הקצאה פעיל @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/hr-HR/Resources.resw b/src/Foundry/Strings/hr-HR/Resources.resw index d449b047f..6bbfe2710 100644 --- a/src/Foundry/Strings/hr-HR/Resources.resw +++ b/src/Foundry/Strings/hr-HR/Resources.resw @@ -1828,6 +1828,183 @@ Licenciran prema MIT licenci. Dokumentaciju nije bilo moguće otvoriti + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Datoteka certifikata pouzdanog korijenskog CA @@ -1981,6 +2158,12 @@ Licenciran prema MIT licenci. Stanje spremnosti medija + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arapski (Saudijska Arabija) @@ -2191,6 +2374,9 @@ Licenciran prema MIT licenci. Dokumentacija + + Domain Join + Standardno Ethernet povezivanje dostupno je prema zadanim postavkama. Konfigurirajte ovaj odjeljak samo ako vaša žična mreža zahtijeva 802.1X provjeru autentičnosti poduzeća. @@ -2212,6 +2398,12 @@ Licenciran prema MIT licenci. Dom + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurirajte nazive implementiranih uređaja. @@ -2272,6 +2464,12 @@ Licenciran prema MIT licenci. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktivni način dodjele resursa @@ -3902,4 +4100,10 @@ Primjena ažuriranja ponovno pokreće Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/hu-HU/Resources.resw b/src/Foundry/Strings/hu-HU/Resources.resw index 8c4216cc8..ac432b07c 100644 --- a/src/Foundry/Strings/hu-HU/Resources.resw +++ b/src/Foundry/Strings/hu-HU/Resources.resw @@ -1828,6 +1828,183 @@ Licenc az MIT licenc alatt. A dokumentációt nem lehetett megnyitni + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Megbízható root CA tanúsítványfájl @@ -1981,6 +2158,12 @@ Licenc az MIT licenc alatt. Az adathordozó készenléti állapota + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arab (Szaúd-Arábia) @@ -2191,6 +2374,9 @@ Licenc az MIT licenc alatt. Dokumentáció + + Domain Join + A szabványos Ethernet kapcsolat alapértelmezés szerint elérhető. Ezt a szakaszt csak akkor konfigurálja, ha vezetékes hálózata 802.1X vállalati hitelesítést igényel. @@ -2212,6 +2398,12 @@ Licenc az MIT licenc alatt. Otthon + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurálja a telepített eszközök nevét. @@ -2272,6 +2464,12 @@ Licenc az MIT licenc alatt. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktív kiépítési mód @@ -3902,4 +4100,10 @@ A frissítés alkalmazása újraindítja a Foundry OSD-t. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/it-IT/Resources.resw b/src/Foundry/Strings/it-IT/Resources.resw index 35aecff7a..7e1d485a2 100644 --- a/src/Foundry/Strings/it-IT/Resources.resw +++ b/src/Foundry/Strings/it-IT/Resources.resw @@ -1828,6 +1828,183 @@ Concesso in licenza con la licenza MIT. Impossibile aprire la documentazione + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + File del certificato della CA radice attendibile @@ -1981,6 +2158,12 @@ Concesso in licenza con la licenza MIT. Stato di preparazione del supporto + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabo (Arabia Saudita) @@ -2191,6 +2374,9 @@ Concesso in licenza con la licenza MIT. Documentazione + + Domain Join + La connettività standard Ethernet è disponibile per impostazione predefinita. Configura questa sezione solo se la tua rete cablata richiede l'autenticazione aziendale 802.1X. @@ -2212,6 +2398,12 @@ Concesso in licenza con la licenza MIT. Casa + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configura i nomi dei dispositivi distribuiti. @@ -2272,6 +2464,12 @@ Concesso in licenza con la licenza MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Modalità di provisioning attiva @@ -3902,4 +4100,10 @@ L’applicazione dell’aggiornamento riavvia Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/ja-JP/Resources.resw b/src/Foundry/Strings/ja-JP/Resources.resw index 936bff1e9..9466a52bf 100644 --- a/src/Foundry/Strings/ja-JP/Resources.resw +++ b/src/Foundry/Strings/ja-JP/Resources.resw @@ -1828,6 +1828,183 @@ MITライセンスに基づいてライセンスされています。 ドキュメントを開けませんでした + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + 信頼されたルート CA 証明書ファイル @@ -1981,6 +2158,12 @@ MITライセンスに基づいてライセンスされています。 メディアの準備状況 + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + アラビア語 (サウジアラビア) @@ -2191,6 +2374,9 @@ MITライセンスに基づいてライセンスされています。 ドキュメント + + Domain Join + 標準の Ethernet 接続はデフォルトで利用可能です。このセクションは、有線ネットワークで 802.1X エンタープライズ認証が必要な場合にのみ設定します。 @@ -2212,6 +2398,12 @@ MITライセンスに基づいてライセンスされています。 ホーム + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + 展開されたデバイス名を構成します。 @@ -2272,6 +2464,12 @@ MITライセンスに基づいてライセンスされています。 Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + アクティブなプロビジョニング モード @@ -3902,4 +4100,10 @@ MITライセンスに基づいてライセンスされています。 Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/ko-KR/Resources.resw b/src/Foundry/Strings/ko-KR/Resources.resw index 41ba2d039..95e10ec28 100644 --- a/src/Foundry/Strings/ko-KR/Resources.resw +++ b/src/Foundry/Strings/ko-KR/Resources.resw @@ -1828,6 +1828,183 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 문서를 열 수 없습니다. + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + 신뢰할 수 있는 루트 CA 인증서 파일 @@ -1981,6 +2158,12 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 미디어 준비 상태 + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + 아랍어(사우디아라비아) @@ -2191,6 +2374,9 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 문서 + + Domain Join + 기본적으로 표준 Ethernet 연결을 사용할 수 있습니다. 유선 네트워크에 802.1X 기업 인증이 필요한 경우에만 이 섹션을 구성하십시오. @@ -2212,6 +2398,12 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 홈 + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + 배포된 장치 이름을 구성합니다. @@ -2272,6 +2464,12 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + 활성 프로비저닝 모드 @@ -3902,4 +4100,10 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/lt-LT/Resources.resw b/src/Foundry/Strings/lt-LT/Resources.resw index 0f6b6b541..58971fa16 100644 --- a/src/Foundry/Strings/lt-LT/Resources.resw +++ b/src/Foundry/Strings/lt-LT/Resources.resw @@ -1828,6 +1828,183 @@ Licencijuota pagal MIT licenciją. Nepavyko atidaryti dokumentų + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Patikimas šakninio CA sertifikato failas @@ -1981,6 +2158,12 @@ Licencijuota pagal MIT licenciją. Laikmenos parengties būsena + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabų (Saudo Arabija) @@ -2191,6 +2374,9 @@ Licencijuota pagal MIT licenciją. Dokumentacija + + Domain Join + Standartinis Ethernet ryšys galimas pagal numatytuosius nustatymus. Konfigūruokite šį skyrių, tik jei jūsų laidiniam tinklui reikalingas 802.1X įmonės autentifikavimas. @@ -2212,6 +2398,12 @@ Licencijuota pagal MIT licenciją. Pradžia + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigūruokite įdiegtų įrenginių pavadinimus. @@ -2272,6 +2464,12 @@ Licencijuota pagal MIT licenciją. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktyvus parengimo režimas @@ -3902,4 +4100,10 @@ Pritaikius naujinimą Foundry OSD paleidžiama iš naujo. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/lv-LV/Resources.resw b/src/Foundry/Strings/lv-LV/Resources.resw index 4f47f1bb0..3c7a77cb3 100644 --- a/src/Foundry/Strings/lv-LV/Resources.resw +++ b/src/Foundry/Strings/lv-LV/Resources.resw @@ -1828,6 +1828,183 @@ Licencēts saskaņā ar MIT licenci. Dokumentāciju nevarēja atvērt + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Uzticams saknes CA sertifikāta fails @@ -1981,6 +2158,12 @@ Licencēts saskaņā ar MIT licenci. Datu nesēja gatavības statuss + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arābu (Saūda Arābija) @@ -2191,6 +2374,9 @@ Licencēts saskaņā ar MIT licenci. Dokumentācija + + Domain Join + Standarta Ethernet savienojums ir pieejams pēc noklusējuma. Konfigurējiet šo sadaļu tikai tad, ja jūsu vadu tīklam ir nepieciešama 802.1X uzņēmuma autentifikācija. @@ -2212,6 +2398,12 @@ Licencēts saskaņā ar MIT licenci. Sākums + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurējiet izvietoto ierīču nosaukumus. @@ -2272,6 +2464,12 @@ Licencēts saskaņā ar MIT licenci. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktīvais nodrošināšanas režīms @@ -3902,4 +4100,10 @@ Atjauninājuma lietošana restartē Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/nb-NO/Resources.resw b/src/Foundry/Strings/nb-NO/Resources.resw index c8360041d..b4184b2d9 100644 --- a/src/Foundry/Strings/nb-NO/Resources.resw +++ b/src/Foundry/Strings/nb-NO/Resources.resw @@ -1828,6 +1828,183 @@ Lisensiert under MIT-lisensen. Dokumentasjonen kunne ikke åpnes + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Klarert rot-CA-sertifikatfil @@ -1981,6 +2158,12 @@ Lisensiert under MIT-lisensen. Status for medieberedskap + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabisk (Saudi-Arabia) @@ -2191,6 +2374,9 @@ Lisensiert under MIT-lisensen. Dokumentasjon + + Domain Join + Standard Ethernet-tilkobling er tilgjengelig som standard. Konfigurer denne delen bare hvis det kablede nettverket ditt krever 802.1X enterprise-autentisering. @@ -2212,6 +2398,12 @@ Lisensiert under MIT-lisensen. Hjem + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurer navn på distribuerte enheter. @@ -2272,6 +2464,12 @@ Lisensiert under MIT-lisensen. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktiv klargjøringsmodus @@ -3902,4 +4100,10 @@ Oppdateringen starter Foundry OSD på nytt. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/nl-NL/Resources.resw b/src/Foundry/Strings/nl-NL/Resources.resw index 62f268ca9..6eaa23e28 100644 --- a/src/Foundry/Strings/nl-NL/Resources.resw +++ b/src/Foundry/Strings/nl-NL/Resources.resw @@ -1828,6 +1828,183 @@ Gelicentieerd onder de MIT-licentie. Documentatie kon niet worden geopend + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Vertrouwd root-CA-certificaatbestand @@ -1981,6 +2158,12 @@ Gelicentieerd onder de MIT-licentie. Status van mediagereedheid + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabisch (Saoedi-Arabië) @@ -2191,6 +2374,9 @@ Gelicentieerd onder de MIT-licentie. Documentatie + + Domain Join + Standaard Ethernet-connectiviteit is standaard beschikbaar. Configureer deze sectie alleen als uw bekabelde netwerk 802.1X bedrijfsverificatie vereist. @@ -2212,6 +2398,12 @@ Gelicentieerd onder de MIT-licentie. Thuis + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configureer namen van geïmplementeerde apparaten. @@ -2272,6 +2464,12 @@ Gelicentieerd onder de MIT-licentie. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Actieve provisioningsmodus @@ -3902,4 +4100,10 @@ Het toepassen van de update start Foundry OSD opnieuw. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/pl-PL/Resources.resw b/src/Foundry/Strings/pl-PL/Resources.resw index 5d9e436d7..3605486c1 100644 --- a/src/Foundry/Strings/pl-PL/Resources.resw +++ b/src/Foundry/Strings/pl-PL/Resources.resw @@ -1828,6 +1828,183 @@ Licencjonowany na licencji MIT. Nie można otworzyć dokumentacji + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Plik certyfikatu zaufanego głównego urzędu certyfikacji @@ -1981,6 +2158,12 @@ Licencjonowany na licencji MIT. Stan gotowości nośnika + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabski (Arabia Saudyjska) @@ -2191,6 +2374,9 @@ Licencjonowany na licencji MIT. Dokumentacja + + Domain Join + Domyślnie dostępna jest standardowa łączność Ethernet. Skonfiguruj tę sekcję tylko wtedy, gdy Twoja sieć przewodowa wymaga uwierzytelniania korporacyjnego 802.1X. @@ -2212,6 +2398,12 @@ Licencjonowany na licencji MIT. Dom + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Skonfiguruj nazwy wdrożonych urządzeń. @@ -2272,6 +2464,12 @@ Licencjonowany na licencji MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktywny tryb aprowizacji @@ -3902,4 +4100,10 @@ Zastosowanie aktualizacji uruchomi Foundry OSD ponownie. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/pt-BR/Resources.resw b/src/Foundry/Strings/pt-BR/Resources.resw index 547697757..d0dd4e47d 100644 --- a/src/Foundry/Strings/pt-BR/Resources.resw +++ b/src/Foundry/Strings/pt-BR/Resources.resw @@ -1828,6 +1828,183 @@ Licenciado sob a licença MIT. Não foi possível abrir a documentação + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Arquivo de certificado de CA raiz confiável @@ -1981,6 +2158,12 @@ Licenciado sob a licença MIT. Status de preparação da mídia + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Árabe (Arábia Saudita) @@ -2191,6 +2374,9 @@ Licenciado sob a licença MIT. Documentação + + Domain Join + A conectividade padrão Ethernet está disponível por padrão. Configure esta seção somente se sua rede com fio exigir autenticação empresarial 802.1X. @@ -2212,6 +2398,12 @@ Licenciado sob a licença MIT. Página inicial + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configure os nomes dos dispositivos implantados. @@ -2272,6 +2464,12 @@ Licenciado sob a licença MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Modo de provisionamento ativo @@ -3902,4 +4100,10 @@ A aplicação da atualização reinicia o Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/pt-PT/Resources.resw b/src/Foundry/Strings/pt-PT/Resources.resw index 6a27501de..e3e560581 100644 --- a/src/Foundry/Strings/pt-PT/Resources.resw +++ b/src/Foundry/Strings/pt-PT/Resources.resw @@ -1828,6 +1828,183 @@ Licenciado sob a licença MIT. Não foi possível abrir a documentação + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Arquivo de certificado de CA raiz confiável @@ -1981,6 +2158,12 @@ Licenciado sob a licença MIT. Estado de preparação do suporte de dados + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Árabe (Arábia Saudita) @@ -2191,6 +2374,9 @@ Licenciado sob a licença MIT. Documentação + + Domain Join + A conectividade padrão Ethernet está disponível por padrão. Configure esta seção somente se sua rede com fio exigir autenticação empresarial 802.1X. @@ -2212,6 +2398,12 @@ Licenciado sob a licença MIT. Página inicial + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configure os nomes dos dispositivos implementados. @@ -2272,6 +2464,12 @@ Licenciado sob a licença MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Modo de aprovisionamento ativo @@ -3902,4 +4100,10 @@ A aplicação da atualização reinicia o Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/ro-RO/Resources.resw b/src/Foundry/Strings/ro-RO/Resources.resw index d1a73b356..9a3aef91b 100644 --- a/src/Foundry/Strings/ro-RO/Resources.resw +++ b/src/Foundry/Strings/ro-RO/Resources.resw @@ -1828,6 +1828,183 @@ Licențiat sub licența MIT. Documentația nu a putut fi deschisă + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Fișier de certificat CA rădăcină de încredere @@ -1981,6 +2158,12 @@ Licențiat sub licența MIT. Starea de pregătire a suportului media + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabă (Arabia Saudită) @@ -2191,6 +2374,9 @@ Licențiat sub licența MIT. Documentare + + Domain Join + Conectivitatea standard Ethernet este disponibilă implicit. Configurați această secțiune numai dacă rețeaua dvs. cu fir necesită autentificare de întreprindere 802.1X. @@ -2212,6 +2398,12 @@ Licențiat sub licența MIT. Acasă + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Configurați numele dispozitivelor implementate. @@ -2272,6 +2464,12 @@ Licențiat sub licența MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Mod de furnizare activ @@ -3902,4 +4100,10 @@ Aplicarea actualizării repornește Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/ru-RU/Resources.resw b/src/Foundry/Strings/ru-RU/Resources.resw index 3f87ff3ed..17c5d03b7 100644 --- a/src/Foundry/Strings/ru-RU/Resources.resw +++ b/src/Foundry/Strings/ru-RU/Resources.resw @@ -1828,6 +1828,183 @@ Документацию невозможно открыть + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Файл сертификата доверенного корневого центра сертификации @@ -1981,6 +2158,12 @@ Состояние готовности носителя + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Арабский (Саудовская Аравия) @@ -2191,6 +2374,9 @@ Документация + + Domain Join + Стандартное подключение Ethernet доступно по умолчанию. Настройте этот раздел только в том случае, если ваша проводная сеть требует корпоративной аутентификации 802.1X. @@ -2212,6 +2398,12 @@ Главная + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Настройте имена развернутых устройств. @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Активный режим подготовки @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/sk-SK/Resources.resw b/src/Foundry/Strings/sk-SK/Resources.resw index dfd5993a1..5d1007333 100644 --- a/src/Foundry/Strings/sk-SK/Resources.resw +++ b/src/Foundry/Strings/sk-SK/Resources.resw @@ -1828,6 +1828,183 @@ Licencované podľa licencie MIT. Dokumentáciu nebolo možné otvoriť + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Súbor certifikátu dôveryhodnej koreňovej CA @@ -1981,6 +2158,12 @@ Licencované podľa licencie MIT. Stav pripravenosti média + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabčina (Saudská Arábia) @@ -2191,6 +2374,9 @@ Licencované podľa licencie MIT. Dokumentácia + + Domain Join + Štandardne je k dispozícii štandardné pripojenie Ethernet. Túto časť nakonfigurujte len vtedy, ak vaša káblová sieť vyžaduje podnikové overenie 802.1X. @@ -2212,6 +2398,12 @@ Licencované podľa licencie MIT. Domov + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Nakonfigurujte názvy nasadených zariadení. @@ -2272,6 +2464,12 @@ Licencované podľa licencie MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktívny režim poskytovania @@ -3902,4 +4100,10 @@ Použitie aktualizácie reštartuje Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/sl-SI/Resources.resw b/src/Foundry/Strings/sl-SI/Resources.resw index 6b64948f6..11626a521 100644 --- a/src/Foundry/Strings/sl-SI/Resources.resw +++ b/src/Foundry/Strings/sl-SI/Resources.resw @@ -1828,6 +1828,183 @@ Licencirano pod licenco MIT. Dokumentacije ni bilo mogoče odpreti + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Datoteka potrdila zaupanja vrednega korenskega CA @@ -1981,6 +2158,12 @@ Licencirano pod licenco MIT. Stanje pripravljenosti medija + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arabščina (Saudova Arabija) @@ -2191,6 +2374,9 @@ Licencirano pod licenco MIT. Dokumentacija + + Domain Join + Standardna Ethernet povezljivost je privzeto na voljo. Ta razdelek konfigurirajte samo, če vaše žično omrežje zahteva preverjanje pristnosti podjetja 802.1X. @@ -2212,6 +2398,12 @@ Licencirano pod licenco MIT. domov + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurirajte imena nameščenih naprav. @@ -2272,6 +2464,12 @@ Licencirano pod licenco MIT. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktivni način zagotavljanja @@ -3902,4 +4100,10 @@ Uporaba posodobitve znova zažene Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/sr-Latn-RS/Resources.resw b/src/Foundry/Strings/sr-Latn-RS/Resources.resw index b09620ce7..d5a8141fb 100644 --- a/src/Foundry/Strings/sr-Latn-RS/Resources.resw +++ b/src/Foundry/Strings/sr-Latn-RS/Resources.resw @@ -1828,6 +1828,183 @@ Licencirano pod MIT licencom. Dokumentacija nije mogla da se otvori + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Datoteka sertifikata pouzdanog root CA @@ -1981,6 +2158,12 @@ Licencirano pod MIT licencom. Status spremnosti medijuma + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + arapski (Saudijska Arabija) @@ -2191,6 +2374,9 @@ Licencirano pod MIT licencom. Dokumentacija + + Domain Join + Standardno Ethernet povezivanje je podrazumevano dostupno. Konfigurišite ovaj odeljak samo ako vaša žičana mreža zahteva 802.1X autentifikaciju preduzeća. @@ -2212,6 +2398,12 @@ Licencirano pod MIT licencom. Home + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurišite nazive raspoređenih uređaja. @@ -2272,6 +2464,12 @@ Licencirano pod MIT licencom. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktivan režim obezbeđivanja @@ -3902,4 +4100,10 @@ Primena ažuriranja ponovo pokreće Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/sv-SE/Resources.resw b/src/Foundry/Strings/sv-SE/Resources.resw index 8f15b66ea..c47563391 100644 --- a/src/Foundry/Strings/sv-SE/Resources.resw +++ b/src/Foundry/Strings/sv-SE/Resources.resw @@ -1828,6 +1828,183 @@ Licensierad under MIT-licensen. Dokumentationen kunde inte öppnas + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Betrodd rot-CA-certifikatfil @@ -1981,6 +2158,12 @@ Licensierad under MIT-licensen. Status för medieberedskap + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arabiska (Saudiarabien) @@ -2191,6 +2374,9 @@ Licensierad under MIT-licensen. Dokumentation + + Domain Join + Standard Ethernet-anslutning är tillgänglig som standard. Konfigurera detta avsnitt endast om ditt trådbundna nätverk kräver 802.1X företagsautentisering. @@ -2212,6 +2398,12 @@ Licensierad under MIT-licensen. Hem + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Konfigurera namn på utrullade enheter. @@ -2272,6 +2464,12 @@ Licensierad under MIT-licensen. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Aktivt provisioneringsläge @@ -3902,4 +4100,10 @@ Uppdateringen startar om Foundry OSD. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/th-TH/Resources.resw b/src/Foundry/Strings/th-TH/Resources.resw index 5001782a7..6e1bf9180 100644 --- a/src/Foundry/Strings/th-TH/Resources.resw +++ b/src/Foundry/Strings/th-TH/Resources.resw @@ -1828,6 +1828,183 @@ ไม่สามารถเปิดเอกสารได้ + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + ไฟล์ใบรับรอง root CA ที่เชื่อถือได้ @@ -1981,6 +2158,12 @@ สถานะความพร้อมของสื่อ + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + อาหรับ (ซาอุดีอาระเบีย) @@ -2191,6 +2374,9 @@ เอกสารประกอบ + + Domain Join + การเชื่อมต่อมาตรฐาน Ethernet พร้อมใช้งานตามค่าเริ่มต้น กำหนดค่าส่วนนี้เฉพาะในกรณีที่เครือข่ายแบบใช้สายของคุณต้องมีการรับรองความถูกต้องระดับองค์กร 802.1X @@ -2212,6 +2398,12 @@ บ้าน + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + กำหนดค่าชื่ออุปกรณ์ที่ปรับใช้ @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + โหมดการจัดสรรที่ใช้งานอยู่ @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/tr-TR/Resources.resw b/src/Foundry/Strings/tr-TR/Resources.resw index 4c89b6b5b..33fd94f16 100644 --- a/src/Foundry/Strings/tr-TR/Resources.resw +++ b/src/Foundry/Strings/tr-TR/Resources.resw @@ -1828,6 +1828,183 @@ MIT Lisansı kapsamında lisanslanmıştır. Belgeler açılamadı + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Güvenilen kök CA sertifika dosyası @@ -1981,6 +2158,12 @@ MIT Lisansı kapsamında lisanslanmıştır. Medya hazırlık durumu + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + Arapça (Suudi Arabistan) @@ -2191,6 +2374,9 @@ MIT Lisansı kapsamında lisanslanmıştır. Dokümantasyon + + Domain Join + Standart Ethernet bağlantısı varsayılan olarak mevcuttur. Bu bölümü yalnızca kablolu ağınız 802.1X kurumsal kimlik doğrulaması gerektiriyorsa yapılandırın. @@ -2212,6 +2398,12 @@ MIT Lisansı kapsamında lisanslanmıştır. Ana sayfa + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Dağıtılan cihaz adlarını yapılandırın. @@ -2272,6 +2464,12 @@ MIT Lisansı kapsamında lisanslanmıştır. Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Etkin temel hazırlık modu @@ -3902,4 +4100,10 @@ Güncellemeyi uygulamak Foundry OSD’yi yeniden başlatır. Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/uk-UA/Resources.resw b/src/Foundry/Strings/uk-UA/Resources.resw index 5e482aa05..8a16da120 100644 --- a/src/Foundry/Strings/uk-UA/Resources.resw +++ b/src/Foundry/Strings/uk-UA/Resources.resw @@ -1828,6 +1828,183 @@ Не вдалося відкрити документацію + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + Файл сертифіката довіреного кореневого ЦС @@ -1981,6 +2158,12 @@ Стан готовності носія + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + арабська (Саудівська Аравія) @@ -2191,6 +2374,9 @@ Документація + + Domain Join + Стандартне підключення Ethernet доступне за замовчуванням. Налаштуйте цей розділ, лише якщо ваша дротова мережа потребує корпоративної автентифікації 802.1X. @@ -2212,6 +2398,12 @@ додому + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + Налаштуйте імена розгорнутих пристроїв. @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + Активний режим ініціалізації @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/zh-CN/Resources.resw b/src/Foundry/Strings/zh-CN/Resources.resw index 659695a39..bd248057e 100644 --- a/src/Foundry/Strings/zh-CN/Resources.resw +++ b/src/Foundry/Strings/zh-CN/Resources.resw @@ -1828,6 +1828,183 @@ 文档无法打开 + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + 受信任的根 CA 证书文件 @@ -1981,6 +2158,12 @@ 媒体准备状态 + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + 阿拉伯语(沙特阿拉伯) @@ -2191,6 +2374,9 @@ 文档 + + Domain Join + 默认情况下,标准 Ethernet 连接可用。仅当您的有线网络需要 802.1X 企业身份验证时才配置此部分。 @@ -2212,6 +2398,12 @@ 首页 + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + 配置已部署设备的名称。 @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + 活动配置模式 @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/Strings/zh-TW/Resources.resw b/src/Foundry/Strings/zh-TW/Resources.resw index ad380e284..bf8f4e103 100644 --- a/src/Foundry/Strings/zh-TW/Resources.resw +++ b/src/Foundry/Strings/zh-TW/Resources.resw @@ -1828,6 +1828,183 @@ 文件無法開啟 + + Activate + + + Check the target DNS domain, destination label and distinguished name. Destinations must belong to the catalog domain, with at most 1,024 entries. + + + Enter a valid target DNS domain and qualified account before setting a password. Passwords must fit the supported size and contain no NUL characters. + + + Deactivate + + + Discovering destinations from this computer's AD domain... + + + Discovery was canceled. Manual destination controls remain available. + + + Discovery completed. Select the destinations to import. + + + This preview is incomplete. Directory limits, unreadable entries or referrals may have omitted destinations. Selected visible entries can still be imported. + + + Discovery timed out. Manual destination controls remain available. + + + Discovery is unavailable. Check computer domain membership, connectivity and directory read access, or add destinations manually. + + + Import was not applied. The target and catalog domain must match the discovered computer domain, and the resulting catalog must fit the supported bounds. + + + Selected destinations were imported. + + + This provisioning mode is inactive. + + + Domain joining is ready. + + + Activating this mode replaces the current Domain Join or Windows Autopilot mode. Inactive nonsecret settings are retained; domain credentials are cleared when their ownership changes. + + + Replace active provisioning mode? + + + Select at least one destination from the preview. + + + Enter the domain account. + + + The target DNS domain must match the destination catalog domain. Remove old catalog entries before changing domains. + + + Set a DNS domain for the destination catalog. + + + Select an existing default destination or clear the default. + + + Enter the target DNS domain. + + + The catalog contains duplicate destination distinguished names. + + + The catalog contains duplicate destination identities. + + + Enter a valid account within the supported length. + + + Enter a valid catalog DNS domain. + + + A destination distinguished name is invalid. + + + Enter a valid target DNS domain. + + + Each destination needs a label of at most 120 characters. + + + A destination has an invalid identity. Remove and add it again. + + + Enable media password protection on the General page and enter a valid media password. + + + Add destinations before enabling selection, or clear the selection and default. + + + Every destination must be inside the catalog domain. + + + Enter the account password for this domain and account. + + + Use DOMAIN\user or user@domain for the account. + + + The catalog supports at most 1,024 destinations. + + + Select a supported domain joining mode. + + + Account (DOMAIN\user or user@domain) + + + Add destination + + + Allow destination selection during deployment + + + Cancel discovery + + + Clear default + + + Use a qualified account with administrator-delegated join and placement permissions. Foundry does not audit permissions. The password is cleared when the domain, account or active mode changes. + + + Domain credentials + + + Default destination (optional) + + + Without a catalog, technicians may type an optional OU. With a catalog, destinations are restricted to its entries. A fixed default has no picker; a picker without a default requires selection. Existing computer accounts move to the selected destination. + + + Destination selection + + + Discover destinations + + + Uses this computer's AD domain and your current Windows identity. Preview and select entries explicitly; import preserves manual rows and the default. Discovery does not test join permissions. + + + Import from the computer domain + + + Import selected destinations + + + Enter a label and a valid distinguished name inside the target DNS domain. Manual entries remain available when directory discovery fails. + + + Add a destination manually + + + OU distinguished name + + + Display label + + + Account password + + + Remove + + + Required for Zero Touch and an OU catalog. Optional for interactive deployment without a catalog. + + + Target DNS domain + 受信任的根 CA 憑證文件 @@ -1981,6 +2158,12 @@ 媒體準備狀態 + + Request domain and account credentials during deployment. This mode does not require a media password. + + + Interactive Domain Join + 阿拉伯語(沙烏地阿拉伯) @@ -2191,6 +2374,9 @@ 文件 + + Domain Join + 預設情況下,標準 Ethernet 連線可用。僅當您的有線網路需要 802.1X 企業身份驗證時才配置此部分。 @@ -2212,6 +2398,12 @@ 首頁 + + Request domain credentials during deployment and optionally select a destination OU. + + + Interactive Domain Join + 設定已部署裝置的名稱。 @@ -2272,6 +2464,12 @@ Autopilot + + Join using embedded credentials protected by the General-page media password. + + + Zero Touch Domain Join + 使用中的配置模式 @@ -3902,4 +4100,10 @@ Wi-Fi + + Embed domain credentials using the existing media password configured on the General page. + + + Zero Touch Domain Join + diff --git a/src/Foundry/ViewModels/AutopilotConfigurationViewModel.cs b/src/Foundry/ViewModels/AutopilotConfigurationViewModel.cs index 4e45ce1b6..b5b25b024 100644 --- a/src/Foundry/ViewModels/AutopilotConfigurationViewModel.cs +++ b/src/Foundry/ViewModels/AutopilotConfigurationViewModel.cs @@ -38,6 +38,7 @@ public sealed partial class AutopilotConfigurationViewModel : ObservableObject, private readonly ILogger logger; private bool isApplyingState = true; private bool isSavingState; + private bool isDisposed; private AutopilotProvisioningMode provisioningMode = AutopilotProvisioningMode.JsonProfile; private AutopilotHardwareHashUploadSettings hardwareHashUploadSettings = new(); private AutopilotTenantOnboardingStatus? tenantOnboardingStatus; @@ -447,6 +448,7 @@ private bool HasConnectedTenantInCurrentSession /// public void Dispose() { + isDisposed = true; localizationService.LanguageChanged -= OnLanguageChanged; configurationStateService.StateChanged -= OnConfigurationStateChanged; Profiles.CollectionChanged -= OnProfilesCollectionChanged; @@ -460,17 +462,22 @@ public void Dispose() /// Provisioning mode represented by the current page. public async Task ToggleProvisioningModeAsync(AutopilotProvisioningMode mode) { + FoundryConfigurationDocument baseline = configurationStateService.Current; AutopilotProvisioningModeToggleResult result = AutopilotProvisioningModeToggleEvaluator.Evaluate( - IsAutopilotEnabled, - provisioningMode, + baseline.Autopilot.IsEnabled, + baseline.Autopilot.ProvisioningMode, mode); - if (result.RequiresConfirmation && !await ConfirmProvisioningModeReplacementAsync(mode)) + if ((result.RequiresConfirmation || result.IsEnabled && baseline.DomainJoin.IsEnabled) && + !await ConfirmProvisioningModeReplacementAsync(mode)) { return; } - ApplyProvisioningModeState(result.Mode, result.IsEnabled); + if (isDisposed || !ReferenceEquals(baseline, configurationStateService.Current)) return; + configurationStateService.UpdateProvisioningSelection( + baseline.Autopilot with { ProvisioningMode = result.Mode, IsEnabled = result.IsEnabled }, + baseline.DomainJoin with { IsEnabled = false }); } [RelayCommand(CanExecute = nameof(CanImportProfile))] @@ -1031,26 +1038,12 @@ private void RefreshProvisioningModeState() RetireActiveCertificateCommand.NotifyCanExecuteChanged(); } - private void ApplyProvisioningModeState(AutopilotProvisioningMode mode, bool isEnabled) - { - isApplyingState = true; - try - { - provisioningMode = mode; - IsAutopilotEnabled = isEnabled; - } - finally - { - isApplyingState = false; - } - - RefreshProvisioningModeState(); - SaveState(); - } - private async Task ConfirmProvisioningModeReplacementAsync(AutopilotProvisioningMode requestedMode) { - string currentMode = GetProvisioningModeDisplayName(provisioningMode); + string currentMode = configurationStateService.Current.DomainJoin.IsEnabled + ? localizationService.GetString(configurationStateService.Current.DomainJoin.Mode == DomainJoinMode.Interactive + ? "Nav_InteractiveDomainJoinKey.Title" : "Nav_ZeroTouchDomainJoinKey.Title") + : GetProvisioningModeDisplayName(provisioningMode); string requestedModeName = GetProvisioningModeDisplayName(requestedMode); return await dialogService.ConfirmAsync(new ConfirmationDialogRequest( localizationService.GetString("Autopilot.ModeSwitchConfirmationTitle"), diff --git a/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs b/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs new file mode 100644 index 000000000..aa0aa75a0 --- /dev/null +++ b/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs @@ -0,0 +1,284 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Application; +using Foundry.Core.Services.Configuration; +using Foundry.Services.Configuration; +using Foundry.Services.DomainJoin; +using Foundry.Services.Localization; + +namespace Foundry.ViewModels; + +/// Shares domain metadata, volatile credentials and explicit OU preview/import across both authoring pages. +public sealed partial class DomainJoinConfigurationViewModel : ObservableObject, IDisposable +{ + private readonly IFoundryConfigurationStateService configuration; + private readonly IDomainJoinSecretStateService secrets; + private readonly IDeploymentProtectionSecretStateService protectionSecrets; + private readonly IAuthoringDomainOuDiscoveryService discovery; + private readonly IDialogService dialogs; + private readonly IApplicationLocalizationService localization; + private CancellationTokenSource? discoveryCancellation; + private FoundryConfigurationDocument? previewBaseline; + private DomainOuDiscoveryResult? preview; + private DomainJoinMode pageMode; + private bool applying; + private bool disposed; + private long discoveryRevision; + private string? statusKey; + + public DomainJoinConfigurationViewModel(IFoundryConfigurationStateService configuration, + IDomainJoinSecretStateService secrets, IDeploymentProtectionSecretStateService protectionSecrets, IAuthoringDomainOuDiscoveryService discovery, + IDialogService dialogs, IApplicationLocalizationService localization) + { + this.configuration = configuration; + this.secrets = secrets; + this.protectionSecrets = protectionSecrets; + this.discovery = discovery; + this.dialogs = dialogs; + this.localization = localization; + configuration.StateChanged += OnStateChanged; + secrets.Changed += OnSecretsChanged; + protectionSecrets.Changed += OnProtectionSecretsChanged; + localization.LanguageChanged += OnLanguageChanged; + ApplyState(); + } + + /// Gets persisted destination rows; discovery does not replace this collection until an explicit import. + public ObservableCollection OrganizationalUnits { get; } = []; + /// Gets the bounded read-only directory preview with explicit per-row import selection. + public ObservableCollection PreviewUnits { get; } = []; + public bool IsActive => configuration.Current.DomainJoin.IsEnabled && configuration.Current.DomainJoin.Mode == pageMode; + public string ActionText => Text(IsActive ? "Deactivate" : "Activate"); + public string DocumentationUrl => pageMode == DomainJoinMode.Interactive + ? FoundryApplicationInfo.InteractiveDomainJoinDocumentationUrl : FoundryApplicationInfo.ZeroTouchDomainJoinDocumentationUrl; + public bool CanDiscover => !IsDiscovering && IsActive; + public bool CanImport => preview is not null && PreviewUnits.Count > 0 && ReferenceEquals(previewBaseline, configuration.Current); + public string PreviewDomainText => preview?.ComputerDomain ?? string.Empty; + public string ReadinessText + { + get + { + if (!IsActive) return Text("Inactive"); + DomainJoinValidationResult result = DomainJoinConfigurationValidator.EvaluateReadiness(configuration.Current.DomainJoin, + secrets.HasPassword(Context()), configuration.Current.General.DeploymentProtection.IsEnabled); + return result.IsValid && configuration.IsDomainJoinConfigurationReady ? Text("Ready") + : result.Issues.Count > 0 ? Text("Validation." + result.Issues[0].Code) : Text("Validation.MediaProtectionRequired"); + } + } + + [ObservableProperty] + public partial string DomainName { get; set; } = string.Empty; + [ObservableProperty] + public partial string AccountName { get; set; } = string.Empty; + /// Enables a deployment picker restricted to the authored catalog. + [ObservableProperty] + public partial bool AllowOuSelectionDuringDeployment { get; set; } + /// Gets or sets the optional stable catalog default, independent of discovery row identities. + [ObservableProperty] + public partial DomainJoinOrganizationalUnitEntryViewModel? SelectedDefaultOu { get; set; } + [ObservableProperty] + public partial string ManualDisplayName { get; set; } = string.Empty; + [ObservableProperty] + public partial string ManualDistinguishedName { get; set; } = string.Empty; + [ObservableProperty] + public partial string StatusText { get; set; } = string.Empty; + [ObservableProperty] + public partial bool IsDiscovering { get; set; } + /// Invalidates PasswordBox content when volatile secret ownership or profile state changes. + [ObservableProperty] + public partial int SecretStateVersion { get; set; } + + /// Selects the page presentation without changing the configured provisioning mode. + public void SetPageMode(DomainJoinMode mode) + { + pageMode = mode; + RefreshPresentation(); + } + + [RelayCommand] + private async Task ToggleModeAsync() + { + FoundryConfigurationDocument baseline = configuration.Current; + bool deactivate = baseline.DomainJoin.IsEnabled && baseline.DomainJoin.Mode == pageMode; + bool replace = !deactivate && (baseline.Autopilot.IsEnabled || baseline.DomainJoin.IsEnabled); + if (replace && !await dialogs.ConfirmAsync(new(Text("ReplacementTitle"), Text("ReplacementMessage"), Text("Activate"), localization.GetString("Common.Cancel"), true))) return; + if (disposed || !ReferenceEquals(baseline, configuration.Current)) return; + configuration.UpdateProvisioningSelection(baseline.Autopilot with { IsEnabled = false }, + baseline.DomainJoin with { IsEnabled = !deactivate, Mode = pageMode }); + } + + /// Copies matching live credentials for PasswordBox synchronization; the caller clears the buffer. + public char[]? GetPasswordCopy() => IsActive && pageMode == DomainJoinMode.Automatic ? secrets.GetPasswordCopy(Context()) : null; + + /// Stores only a context-bound owned password; incomplete identities cannot own a secret. + public void SetPassword(ReadOnlySpan password) + { + if (!IsActive || pageMode != DomainJoinMode.Automatic) return; + try { secrets.SetPassword(Context(), password); SetStatus(null); } + catch (ArgumentException) { secrets.Clear(); SetStatus("CredentialInputInvalid"); } + } + + [RelayCommand] + private void AddOrganizationalUnit() + { + try + { + DomainJoinSettings current = configuration.Current.DomainJoin; + var unit = new DomainJoinOrganizationalUnitSettings + { Id = Guid.NewGuid().ToString("D"), DisplayName = ManualDisplayName.Trim(), DistinguishedName = ManualDistinguishedName.Trim() }; + Save(DomainJoinOrganizationalUnitCatalog.Merge(current, current.DomainName ?? string.Empty, (DomainJoinOrganizationalUnitSettings[])[unit])); + ManualDisplayName = string.Empty; + ManualDistinguishedName = string.Empty; + SetStatus(null); + } + catch (ArgumentException) { SetStatus("CatalogInputInvalid"); } + } + + /// Removes an authored row and its default, while retaining unrelated destinations. + public void RemoveOrganizationalUnit(DomainJoinOrganizationalUnitEntryViewModel row) + { + Save(DomainJoinOrganizationalUnitCatalog.Remove(configuration.Current.DomainJoin, row.Settings.Id)); + } + + [RelayCommand] + private void ClearDefault() => SelectedDefaultOu = null; + + [RelayCommand(CanExecute = nameof(CanDiscover))] + private async Task DiscoverAsync() + { + CancelDiscovery(); + long revision = ++discoveryRevision; + FoundryConfigurationDocument baseline = configuration.Current; + var cancellation = new CancellationTokenSource(); + discoveryCancellation = cancellation; + IsDiscovering = true; + SetStatus("Discovering"); + RefreshPresentation(); + try + { + DomainOuDiscoveryResult result = await discovery.DiscoverAsync(cancellation.Token); + if (disposed || revision != discoveryRevision || !ReferenceEquals(baseline, configuration.Current)) return; + preview = result; + previewBaseline = baseline; + foreach (DomainJoinOrganizationalUnitSettings unit in result.Candidates.OrderBy(unit => unit.DistinguishedName, StringComparer.OrdinalIgnoreCase)) + PreviewUnits.Add(new(unit)); + SetStatus(result.Status switch + { + DomainOuDiscoveryStatus.Complete => "DiscoveryComplete", + DomainOuDiscoveryStatus.Incomplete => "DiscoveryIncomplete", + DomainOuDiscoveryStatus.Canceled => "DiscoveryCanceled", + _ => result.ErrorCode == "Timeout" ? "DiscoveryTimeout" : "DiscoveryUnavailable" + }); + } + finally + { + if (revision == discoveryRevision) + { + IsDiscovering = false; + discoveryCancellation = null; + RefreshPresentation(); + } + cancellation.Dispose(); + } + } + + [RelayCommand] + private void CancelDiscovery() + { + if (IsDiscovering) SetStatus("DiscoveryCanceled"); + discoveryRevision++; + discoveryCancellation?.Cancel(); + discoveryCancellation = null; + IsDiscovering = false; + preview = null; + previewBaseline = null; + PreviewUnits.Clear(); + RefreshPresentation(); + } + + [RelayCommand(CanExecute = nameof(CanImport))] + private void ImportSelected() + { + if (preview is null || !ReferenceEquals(previewBaseline, configuration.Current)) return; + DomainJoinOrganizationalUnitSettings[] selected = PreviewUnits.Where(row => row.IsSelected).Select(row => row.Settings).ToArray(); + if (selected.Length == 0) { SetStatus("SelectDestinations"); return; } + try { Save(DomainJoinOrganizationalUnitCatalog.Merge(configuration.Current.DomainJoin, preview.ComputerDomain ?? string.Empty, selected)); SetStatus("Imported"); } + catch (ArgumentException) { SetStatus("ImportDomainMismatch"); } + } + + partial void OnDomainNameChanged(string value) + { + if (applying) return; + DomainJoinSettings current = configuration.Current.DomainJoin; + bool domainChanged = DomainJoinCredentialContext.CanonicalizeDomainName(current.DomainName) != DomainJoinCredentialContext.CanonicalizeDomainName(value); + Save(current with + { + DomainName = value, + DefaultOuId = domainChanged ? null : current.DefaultOuId, + AllowOuSelectionDuringDeployment = !domainChanged && current.AllowOuSelectionDuringDeployment + }); + } + partial void OnAccountNameChanged(string value) { if (!applying) Save(configuration.Current.DomainJoin with { AccountName = value }); } + partial void OnAllowOuSelectionDuringDeploymentChanged(bool value) + { + if (!applying) Save(configuration.Current.DomainJoin with { AllowOuSelectionDuringDeployment = OrganizationalUnits.Count > 0 && value }); + } + partial void OnSelectedDefaultOuChanged(DomainJoinOrganizationalUnitEntryViewModel? value) + { + if (!applying) Save(configuration.Current.DomainJoin with { DefaultOuId = value?.Settings.Id }); + } + private DomainJoinCredentialContext Context() => new(configuration.Current.DomainJoin.DomainName ?? string.Empty, configuration.Current.DomainJoin.AccountName ?? string.Empty); + private void Save(DomainJoinSettings settings) => configuration.UpdateDomainJoin(settings); + private string Text(string key) => localization.GetString("DomainJoin." + key); + private void SetStatus(string? key) { statusKey = key; StatusText = key is null ? string.Empty : Text(key); } + private void OnStateChanged(object? sender, EventArgs args) { CancelDiscovery(); ApplyState(); } + private void OnSecretsChanged(object? sender, EventArgs args) { SecretStateVersion++; OnPropertyChanged(nameof(ReadinessText)); } + private void OnProtectionSecretsChanged(object? sender, EventArgs args) => OnPropertyChanged(nameof(ReadinessText)); + private void OnLanguageChanged(object? sender, ApplicationLanguageChangedEventArgs args) { SetStatus(statusKey); RefreshPresentation(); } + + private void ApplyState() + { + applying = true; + try + { + DomainJoinSettings settings = configuration.Current.DomainJoin; + DomainName = settings.DomainName ?? string.Empty; + AccountName = settings.AccountName ?? string.Empty; + AllowOuSelectionDuringDeployment = settings.AllowOuSelectionDuringDeployment; + OrganizationalUnits.Clear(); + foreach (DomainJoinOrganizationalUnitSettings unit in settings.OrganizationalUnits) OrganizationalUnits.Add(new(unit)); + SelectedDefaultOu = OrganizationalUnits.FirstOrDefault(row => string.Equals(row.Settings.Id, settings.DefaultOuId, StringComparison.OrdinalIgnoreCase)); + } + finally { applying = false; } + SecretStateVersion++; + RefreshPresentation(); + } + + private void RefreshPresentation() + { + OnPropertyChanged(nameof(IsActive)); + OnPropertyChanged(nameof(ActionText)); + OnPropertyChanged(nameof(DocumentationUrl)); + OnPropertyChanged(nameof(ReadinessText)); + OnPropertyChanged(nameof(CanDiscover)); + OnPropertyChanged(nameof(CanImport)); + OnPropertyChanged(nameof(PreviewDomainText)); + DiscoverCommand.NotifyCanExecuteChanged(); + ImportSelectedCommand.NotifyCanExecuteChanged(); + } + + /// Discards preview work and subscriptions without clearing credentials owned by the active profile session. + public void Dispose() + { + disposed = true; + CancelDiscovery(); + configuration.StateChanged -= OnStateChanged; + secrets.Changed -= OnSecretsChanged; + protectionSecrets.Changed -= OnProtectionSecretsChanged; + localization.LanguageChanged -= OnLanguageChanged; + } +} diff --git a/src/Foundry/ViewModels/DomainJoinOrganizationalUnitEntryViewModel.cs b/src/Foundry/ViewModels/DomainJoinOrganizationalUnitEntryViewModel.cs new file mode 100644 index 000000000..f22af863f --- /dev/null +++ b/src/Foundry/ViewModels/DomainJoinOrganizationalUnitEntryViewModel.cs @@ -0,0 +1,30 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; +using Foundry.Core.Services.Configuration; + +namespace Foundry.ViewModels; + +/// Displays a portable destination and its decoded hierarchy in an authoring or import preview row. +public sealed partial class DomainJoinOrganizationalUnitEntryViewModel : ObservableObject +{ + public DomainJoinOrganizationalUnitEntryViewModel(DomainJoinOrganizationalUnitSettings settings) + { + Settings = settings; + Hierarchy = DistinguishedNameRules.TryParse(settings.DistinguishedName, out ParsedDistinguishedName parsed) + ? string.Join(" / ", parsed.Rdns.Reverse().SelectMany(rdn => rdn.Attributes) + .Where(attribute => !string.Equals(attribute.Type, "DC", StringComparison.OrdinalIgnoreCase)).Select(attribute => attribute.Value)) + : settings.DistinguishedName; + } + + public DomainJoinOrganizationalUnitSettings Settings { get; } + public string DisplayName => Settings.DisplayName; + public string DistinguishedName => Settings.DistinguishedName; + public string Hierarchy { get; } + + /// Marks a discovered row for an explicit import; selection alone never persists it. + [ObservableProperty] + public partial bool IsSelected { get; set; } +} diff --git a/src/Foundry/ViewModels/StartMediaViewModel.cs b/src/Foundry/ViewModels/StartMediaViewModel.cs index 3c1acbb2d..f668106b9 100644 --- a/src/Foundry/ViewModels/StartMediaViewModel.cs +++ b/src/Foundry/ViewModels/StartMediaViewModel.cs @@ -196,6 +196,9 @@ public StartMediaViewModel( /// public ObservableCollection AutopilotOverviewItems { get; } = []; + /// Gets both exclusive domain joining mode overview rows. + public ObservableCollection DomainJoinOverviewItems { get; } = []; + /// /// Gets the customization overview rows. /// @@ -285,6 +288,9 @@ public StartMediaViewModel( [ObservableProperty] public partial bool IsAutopilotOverviewExpanded { get; set; } + [ObservableProperty] + public partial bool IsDomainJoinOverviewExpanded { get; set; } + [ObservableProperty] public partial bool IsCustomizationOverviewExpanded { get; set; } @@ -297,6 +303,9 @@ public StartMediaViewModel( [ObservableProperty] public partial string AutopilotOverviewSummary { get; set; } = string.Empty; + [ObservableProperty] + public partial string DomainJoinOverviewSummary { get; set; } = string.Empty; + [ObservableProperty] public partial string CustomizationOverviewSummary { get; set; } = string.Empty; @@ -1993,6 +2002,13 @@ private void RebuildConfigurationOverview( IsAutopilotOverviewExpanded = ReplaceOverviewItems( AutopilotOverviewItems, BuildAutopilotOverview(options, overview)); + IsDomainJoinOverviewExpanded = ReplaceOverviewItems(DomainJoinOverviewItems, + (StartConfigurationOverviewItemViewModel[])[ + CreateOverviewItem(ConfigurationOverviewItem.DomainJoinInteractive, overview, "Nav_InteractiveDomainJoinKey.Title", + localizationService.GetString("Nav_InteractiveDomainJoinKey.Description"), ConfigurationNavigationTarget.DomainJoinInteractive), + CreateOverviewItem(ConfigurationOverviewItem.DomainJoinAutomatic, overview, "Nav_ZeroTouchDomainJoinKey.Title", + localizationService.GetString("Nav_ZeroTouchDomainJoinKey.Description"), ConfigurationNavigationTarget.DomainJoinAutomatic) + ]); IsCustomizationOverviewExpanded = ReplaceOverviewItems( CustomizationOverviewItems, BuildCustomizationOverview(configuration, overview)); @@ -2000,6 +2016,7 @@ private void RebuildConfigurationOverview( GeneralConfigurationOverviewSummary = GetOverviewSummary(GeneralConfigurationOverviewItems); NetworkOverviewSummary = GetOverviewSummary(NetworkOverviewItems); AutopilotOverviewSummary = GetOverviewSummary(AutopilotOverviewItems); + DomainJoinOverviewSummary = GetOverviewSummary(DomainJoinOverviewItems); CustomizationOverviewSummary = GetOverviewSummary(CustomizationOverviewItems); } diff --git a/src/Foundry/Views/InteractiveDomainJoinPage.xaml b/src/Foundry/Views/InteractiveDomainJoinPage.xaml new file mode 100644 index 000000000..f5bd7ba36 --- /dev/null +++ b/src/Foundry/Views/InteractiveDomainJoinPage.xaml @@ -0,0 +1,155 @@ + + + + + + + + diff --git a/src/Foundry.Deploy/Views/Wizard/DomainJoinStepView.xaml.cs b/src/Foundry.Deploy/Views/Wizard/DomainJoinStepView.xaml.cs index 93c2f3fe4..d29eb82ca 100644 --- a/src/Foundry.Deploy/Views/Wizard/DomainJoinStepView.xaml.cs +++ b/src/Foundry.Deploy/Views/Wizard/DomainJoinStepView.xaml.cs @@ -11,13 +11,15 @@ namespace Foundry.Deploy.Views.Wizard; /// -/// Bridges the password box, which cannot be data-bound, to the step's owned password buffer. The view is rebuilt -/// each time the technician returns to the step, so it restores the box from that buffer when it loads. +/// Bridges the password editors, which cannot be data-bound, to the step's owned password buffer. The view is +/// rebuilt each time the technician returns to the step, so it restores the masked editor from that buffer when +/// it loads. The reveal button swaps the masked editor for a plain one, as the boot media password dialog does. /// public partial class DomainJoinStepView : UserControl { private DomainJoinStepViewModel? step; - private bool synchronizingPassword; + private bool isPasswordRevealed; + private bool isSynchronizingPasswordEditors; public DomainJoinStepView() { @@ -38,12 +40,12 @@ private void OnUnloaded(object sender, RoutedEventArgs e) { if (step is not null) step.PasswordCleared -= OnPasswordCleared; step = null; - ClearPasswordBox(); + ClearPasswordEditors(); } private void PasswordInput_OnPasswordChanged(object sender, RoutedEventArgs e) { - if (synchronizingPassword || step is null) return; + if (isSynchronizingPasswordEditors || step is null) return; using var secure = PasswordInput.SecurePassword; char[] characters = new char[secure.Length]; IntPtr plaintext = IntPtr.Zero; @@ -60,7 +62,46 @@ private void PasswordInput_OnPasswordChanged(object sender, RoutedEventArgs e) } } - private void OnPasswordCleared(object? sender, EventArgs e) => Dispatcher.Invoke(ClearPasswordBox); + private void PasswordRevealInput_OnTextChanged(object sender, TextChangedEventArgs e) + { + if (isSynchronizingPasswordEditors || step is null) return; + step.SetPassword(PasswordRevealInput.Text); + } + + private void PasswordRevealButton_OnClick(object sender, RoutedEventArgs e) + { + isSynchronizingPasswordEditors = true; + try + { + if (isPasswordRevealed) + { + PasswordInput.Password = PasswordRevealInput.Text; + PasswordRevealInput.Text = string.Empty; + PasswordRevealInput.Visibility = Visibility.Collapsed; + PasswordInput.Visibility = Visibility.Visible; + PasswordRevealButton.ClearValue(StyleProperty); + PasswordInput.Focus(); + } + else + { + PasswordRevealInput.Text = PasswordInput.Password; + PasswordInput.Password = string.Empty; + PasswordInput.Visibility = Visibility.Collapsed; + PasswordRevealInput.Visibility = Visibility.Visible; + PasswordRevealButton.SetResourceReference(StyleProperty, "AccentButtonStyle"); + PasswordRevealInput.Focus(); + PasswordRevealInput.CaretIndex = PasswordRevealInput.Text.Length; + } + + isPasswordRevealed = !isPasswordRevealed; + } + finally + { + isSynchronizingPasswordEditors = false; + } + } + + private void OnPasswordCleared(object? sender, EventArgs e) => Dispatcher.Invoke(ClearPasswordEditors); private void RestorePassword(DomainJoinStepViewModel source) { @@ -68,20 +109,32 @@ private void RestorePassword(DomainJoinStepViewModel source) if (password is null) return; try { - synchronizingPassword = true; + isSynchronizingPasswordEditors = true; PasswordInput.Password = new string(password); } finally { - synchronizingPassword = false; + isSynchronizingPasswordEditors = false; CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(password.AsSpan())); } } - private void ClearPasswordBox() + /// Empties both editors and returns to the masked one, without changing the step's owned password. + private void ClearPasswordEditors() { - synchronizingPassword = true; - try { PasswordInput.Clear(); } - finally { synchronizingPassword = false; } + isSynchronizingPasswordEditors = true; + try + { + PasswordInput.Clear(); + PasswordRevealInput.Clear(); + PasswordRevealInput.Visibility = Visibility.Collapsed; + PasswordInput.Visibility = Visibility.Visible; + PasswordRevealButton.ClearValue(StyleProperty); + isPasswordRevealed = false; + } + finally + { + isSynchronizingPasswordEditors = false; + } } } From e4640fb46d8607bc1313b10049c575a9a205d3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Mon, 5 Oct 2026 22:11:36 +0200 Subject: [PATCH 38/84] fix(postinstall): tolerate recovery edge cases and log from the worker Read the boot identity once per process so every check compares the same value, and give the domain join worker the shared log file. Treat a receipt that cannot be read while an unstoppable worker may still write it as an unknown outcome instead of failing the run. Stop an earlier uncertain action from turning a later continuable failure into a fatal one. Restore the warning-only handling of a journal error while cleaning up a non-sensitive payload. Check the post-install hook against the effective runtime state when validating a custom answer file. Co-Authored-By: Claude Opus 5.5 --- .../Steps/ValidateCustomUnattendStep.cs | 2 +- .../Actions/DomainJoinAction.cs | 5 +++- .../Actions/PreOobeActionExecutor.cs | 11 ++++----- .../Execution/OwnedPayloadCleanup.cs | 23 ++++++++++++------- .../Execution/PreOobeOrchestrator.cs | 3 ++- src/Foundry.PostInstall/Program.cs | 15 ++++++------ 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/Foundry.Deploy/Services/Deployment/Steps/ValidateCustomUnattendStep.cs b/src/Foundry.Deploy/Services/Deployment/Steps/ValidateCustomUnattendStep.cs index fe386f0c4..96cf4a694 100644 --- a/src/Foundry.Deploy/Services/Deployment/Steps/ValidateCustomUnattendStep.cs +++ b/src/Foundry.Deploy/Services/Deployment/Steps/ValidateCustomUnattendStep.cs @@ -28,7 +28,7 @@ protected override async Task ExecuteLiveAsync(DeploymentS return DeploymentStepResult.Failed("The custom answer-file computer name differs from the computer name confirmed for the domain join.", DeploymentFailure.Guard(DeploymentOperationNames.ValidateTarget, DeploymentFailureReasons.InvalidInput, "domain_custom_name_mismatch")); - if (PreOobe.PreOobeContentResolver.IsRequired(context.Request)) + if (PreOobe.PreOobeContentResolver.IsRequired(context.Request, context.RuntimeState)) context.UnattendSnapshot.ValidatePostInstallHook(context.Request.OperatingSystem.Architecture); if (context.UnattendSnapshot.Inspection.HasCommands) { diff --git a/src/Foundry.PostInstall/Actions/DomainJoinAction.cs b/src/Foundry.PostInstall/Actions/DomainJoinAction.cs index c5a893266..65f88b98c 100644 --- a/src/Foundry.PostInstall/Actions/DomainJoinAction.cs +++ b/src/Foundry.PostInstall/Actions/DomainJoinAction.cs @@ -34,7 +34,10 @@ public async Task ExecuteAsync(PreOobeExecutionAction action, catch (Exception error) when (DomainJoinWorker.Recoverable(error)) { process = new(null, string.Empty, TerminationUncertain: true); } Log.Information("Domain join worker ended; exit code {ExitCode}, timed out {TimedOut}, termination uncertain {TerminationUncertain}", process.ExitCode, process.TimedOut, process.TerminationUncertain); - var receipt = phases.Read(); + DomainJoinPhaseReceipt receipt; + try { receipt = phases.Read(); } + // A worker that could not be stopped may still be replacing the receipt; its outcome stays unknown. + catch (IOException) when (process.TerminationUncertain) { receipt = initial; } report = reports.Reconcile(receipt, boot, process.TimedOut ? DomainJoinFailureCode.WorkerTimeout : DomainJoinFailureCode.Interrupted, workerUnsettled: process.TerminationUncertain); bool uncertain = process.TerminationUncertain; diff --git a/src/Foundry.PostInstall/Actions/PreOobeActionExecutor.cs b/src/Foundry.PostInstall/Actions/PreOobeActionExecutor.cs index 2b18b16d5..230d85f0a 100644 --- a/src/Foundry.PostInstall/Actions/PreOobeActionExecutor.cs +++ b/src/Foundry.PostInstall/Actions/PreOobeActionExecutor.cs @@ -9,7 +9,7 @@ namespace Foundry.PostInstall.Actions; public sealed class PreOobeActionExecutor(string root, string windowsRoot, PreOobeExecutionPlan plan, - IPreOobeProcessExecutor processes, ICertificateImporter certificates, string executablePath, string? planHash = null, Func? bootIdentity = null) : IPreOobeActionExecutor + IPreOobeProcessExecutor processes, ICertificateImporter certificates, string executablePath, string planHash, string bootIdentity) : IPreOobeActionExecutor { public async Task ExecuteAsync(PreOobeExecutionAction action, int substep, CancellationToken cancellationToken) { @@ -21,16 +21,15 @@ public async Task ExecuteAsync(PreOobeExecutionAction action, PreOobeBuiltInKind.Network => await new NetworkAction(root, windowsRoot, processes, certificates).ExecuteAsync(action, cancellationToken).ConfigureAwait(false), PreOobeBuiltInKind.Appx or PreOobeBuiltInKind.AiRemoval => await new AppxAction(root, windowsRoot, processes).ExecuteAsync(action, substep, cancellationToken).ConfigureAwait(false), PreOobeBuiltInKind.Activation => await ActivateAsync(cancellationToken).ConfigureAwait(false), - PreOobeBuiltInKind.DomainJoinAndPlacement => await new DomainJoinAction(root, plan, RequireDomainHash(), - (bootIdentity ?? BootIdentityProvider.Read)(), processes, executablePath).ExecuteAsync(action, cancellationToken).ConfigureAwait(false), - PreOobeBuiltInKind.VerifyDomainMembership => await new DomainMembershipVerificationAction(root, plan, RequireDomainHash(), - (bootIdentity ?? BootIdentityProvider.Read)(), new NativeDomainJoin()).ExecuteAsync(action, cancellationToken).ConfigureAwait(false), + PreOobeBuiltInKind.DomainJoinAndPlacement => await new DomainJoinAction(root, plan, planHash, + bootIdentity, processes, executablePath).ExecuteAsync(action, cancellationToken).ConfigureAwait(false), + PreOobeBuiltInKind.VerifyDomainMembership => await new DomainMembershipVerificationAction(root, plan, planHash, + bootIdentity, new NativeDomainJoin()).ExecuteAsync(action, cancellationToken).ConfigureAwait(false), PreOobeBuiltInKind.Cleanup => await CleanupAsync(cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException("Unsupported built-in action.") }; } - private string RequireDomainHash() => planHash ?? throw new InvalidDataException("Domain execution requires its fixed plan hash."); private async Task ActivateAsync(CancellationToken cancellationToken) { diff --git a/src/Foundry.PostInstall/Execution/OwnedPayloadCleanup.cs b/src/Foundry.PostInstall/Execution/OwnedPayloadCleanup.cs index d6e36a815..7be015417 100644 --- a/src/Foundry.PostInstall/Execution/OwnedPayloadCleanup.cs +++ b/src/Foundry.PostInstall/Execution/OwnedPayloadCleanup.cs @@ -28,17 +28,24 @@ public OwnedPayloadCleanupOutcome Dispose(PreOobeExecutionPlan plan, JournalStat (state.UnsafeActionId is null || payload.ConsumerActionIds.Contains(state.UnsafeActionId)); state.PayloadDispositions[payload.RelativePath] = "CleanupPending"; bool disposed = false; - // A journal publication failure must propagate even if best-effort deletion succeeds. - try { journal.Write(state); } - finally { if (!unsafeExecution) disposed = TryDelete(payload); } - if (disposed) state.PayloadDispositions[payload.RelativePath] = "Disposed"; - else + try + { + // For a sensitive payload, a journal publication failure must propagate even if best-effort deletion succeeds. + try { journal.Write(state); } + finally { if (!unsafeExecution) disposed = TryDelete(payload); } + if (disposed) state.PayloadDispositions[payload.RelativePath] = "Disposed"; + else + { + fatal |= payload.IsSensitive && !domain; + domainPending |= domain; + state.HasWarnings = true; + } + journal.Write(state); + } + catch (Exception error) when (!payload.IsSensitive && error is IOException or UnauthorizedAccessException) { - fatal |= payload.IsSensitive && !domain; - domainPending |= domain; state.HasWarnings = true; } - journal.Write(state); } return new(fatal, domainPending); } diff --git a/src/Foundry.PostInstall/Execution/PreOobeOrchestrator.cs b/src/Foundry.PostInstall/Execution/PreOobeOrchestrator.cs index 6e79ab600..114c8c5f2 100644 --- a/src/Foundry.PostInstall/Execution/PreOobeOrchestrator.cs +++ b/src/Foundry.PostInstall/Execution/PreOobeOrchestrator.cs @@ -148,7 +148,8 @@ public async Task RunAsync(PreOobeExecutionPlan plan, Canc if (DisposePayloads(plan, state, false, boot, cleanup).HasFatalSensitiveFailure) throw new IOException("Sensitive input disposal failed."); bool mayContinue = action.CustomAction?.Process?.ErrorPolicy == PreOobeErrorPolicy.Continue || action.BuiltInKind is PreOobeBuiltInKind.Activation || domainAction; - if (!outcome.Succeeded && (!mayContinue || state.UnsafePayloadBootIdentity == boot && !domainAction)) + bool uncertainNow = state.UnsafePayloadBootIdentity == boot && state.UnsafeActionId == action.Id; + if (!outcome.Succeeded && (!mayContinue || uncertainNow && !domainAction)) { state.Status = "Failed"; journal.Write(state); diff --git a/src/Foundry.PostInstall/Program.cs b/src/Foundry.PostInstall/Program.cs index 2cf239029..bcdcb018c 100644 --- a/src/Foundry.PostInstall/Program.cs +++ b/src/Foundry.PostInstall/Program.cs @@ -41,25 +41,26 @@ public static async Task Main(string[] args) var snapshot = await PreOobePlanLoader.LoadAsync(root, CancellationToken.None).ConfigureAwait(false); var plan = snapshot.Plan; string hash = snapshot.Hash; + string logs = OwnedPaths.Resolve(root, "Logs/PreOobe"); + Directory.CreateDirectory(logs); + Log.Logger = FoundryLogConfiguration.CreateFileLogger(Path.Combine(logs, "Foundry.PostInstall.log"), "Foundry.PostInstall", + plan.DiagnosticSessionId, LogEventLevel.Verbose, 5); + // Read once: every check in this process must compare against the same boot identity. + string boot = BootIdentityProvider.Read(); if (args[0] == "--domain-join-worker") { var binding = DomainJoinBinding.Validate(plan); - string boot = BootIdentityProvider.Read(); DomainJoinBinding.RequireRunning(root, plan, hash, boot, binding.JoinAction.Id); var workerResult = await new DomainJoinWorker(root, plan, hash, boot, new NativeDomainJoin(), new DomainComputerAccountDirectory()) .RunAsync(binding.Parameters, CancellationToken.None).ConfigureAwait(false); return workerResult.Join.State == DomainJoinPhaseState.Succeeded ? 0 : 10; } - string logs = OwnedPaths.Resolve(root, "Logs/PreOobe"); - Directory.CreateDirectory(logs); - Log.Logger = FoundryLogConfiguration.CreateFileLogger(Path.Combine(logs, "Foundry.PostInstall.log"), "Foundry.PostInstall", - plan.DiagnosticSessionId, LogEventLevel.Verbose, 5); Log.Information("Post-installation started with {ActionCount} actions", plan.Actions.Count); using var console = new PostInstallConsole(plan, Path.Combine(logs, "Foundry.PostInstall.log")); var executor = new PreOobeActionExecutor(root, windows, plan, new PreOobeProcessExecutor(), - new CertificateImporter(), Environment.ProcessPath ?? throw new InvalidDataException("Runtime path is unavailable."), hash); + new CertificateImporter(), Environment.ProcessPath ?? throw new InvalidDataException("Runtime path is unavailable."), hash, boot); OrchestrationOutcome result = await new PreOobeOrchestrator(root, hash, new ExecutionJournal(root), executor, - BootIdentityProvider.Read, console).RunAsync(plan, CancellationToken.None).ConfigureAwait(false); + () => boot, console).RunAsync(plan, CancellationToken.None).ConfigureAwait(false); Log.Information("Post-installation ended with {Status}; host exit code {HostExitCode}", result.Status, result.ExitCode); console.Complete(result); await console.WaitForSetupAsync().ConfigureAwait(false); From c5bab94729c77ea859c757f429a58a53097a9be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Mon, 5 Oct 2026 22:11:36 +0200 Subject: [PATCH 39/84] fix(domain-join): accept only OUs and join in the default location when one is missing Reject a join target that is not an organizational unit when it is entered, in the authoring pages and in the Deploy step, instead of failing in installed Windows. When the directory answers that the requested OU does not exist, still join the computer in the domain's default location and report placement as failed with a dedicated code, rather than cancelling the join and reporting the domain as unavailable. The console and the log name that outcome, and the worker logs each readiness attempt and its result. Co-Authored-By: Claude Opus 5.5 --- .../DistinguishedNameRulesTests.cs | 12 ++++++++ .../Models/PreOobe/DomainJoinResult.cs | 2 +- .../Configuration/DistinguishedNameRules.cs | 13 ++++++++- .../DomainJoinConfigurationValidator.cs | 3 +- .../DomainJoinStepViewModelTests.cs | 3 ++ .../DomainJoin/DomainJoinPreparedInput.cs | 3 +- .../ViewModels/DomainJoinStepViewModel.cs | 3 +- .../DomainJoinWorkerTests.cs | 16 +++++++++++ .../PostInstallConsoleTests.cs | 7 ++++- .../Console/PostInstallConsole.cs | 2 ++ .../Windows/DomainComputerAccountDirectory.cs | 28 +++++++++++++++++-- .../Windows/DomainJoinWorker.cs | 16 +++++++++-- 12 files changed, 96 insertions(+), 12 deletions(-) diff --git a/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs b/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs index 5ac84057f..0a4966880 100644 --- a/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs +++ b/src/Foundry.Core.Tests/Configuration/DistinguishedNameRulesTests.cs @@ -8,6 +8,18 @@ namespace Foundry.Core.Tests.Configuration; public sealed class DistinguishedNameRulesTests { + [Theory] + [InlineData("OU=Workstations,DC=corp,DC=test", true)] + [InlineData("ou=Laptops,OU=Workstations,DC=corp,DC=test", true)] + [InlineData("CN=Computers,DC=corp,DC=test", false)] + [InlineData("DC=corp,DC=test", false)] + [InlineData("OU=A+CN=B,DC=corp,DC=test", false)] + [InlineData("not-a-dn", false)] + public void OnlyAnOrganizationalUnitIsAJoinTarget(string distinguishedName, bool expected) + { + Assert.Equal(expected, DistinguishedNameRules.IsOrganizationalUnit(distinguishedName)); + } + [Theory] [InlineData("OU=Sales\\, West,DC=contoso,DC=test", "Sales, West")] [InlineData("OU=Sales\\2C West,DC=contoso,DC=test", "Sales, West")] diff --git a/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs b/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs index cd7297f1b..0aded594c 100644 --- a/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs +++ b/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs @@ -20,7 +20,7 @@ public enum DomainJoinFailureCode { InvalidInput, CredentialUnavailable, ContextMismatch, ComputerNameMismatch, DomainUnavailable, ReadinessTimeout, JoinFailed, PlacementFailed, MembershipMismatch, MembershipUnverified, - Interrupted, WorkerTimeout, CleanupFailed, ResultUnavailable, InvalidResult + Interrupted, WorkerTimeout, CleanupFailed, ResultUnavailable, InvalidResult, OrganizationalUnitNotFound } /// Reports one independent phase and an optional native or LDAP numeric diagnostic. diff --git a/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs b/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs index 896120035..579914056 100644 --- a/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs +++ b/src/Foundry.Core/Services/Configuration/DistinguishedNameRules.cs @@ -58,7 +58,18 @@ public static bool IsWithinDomain(string distinguishedName, string dnsDomain) return true; } - /// Detects duplicate authored destinations only; live directory GUIDs remain authoritative. + /// + /// Determines whether the name designates an organizational unit, which is the only container the join can + /// place a computer in. A container such as CN=Computers is rejected here instead of failing at join time. + /// + public static bool IsOrganizationalUnit(string distinguishedName) => + TryParse(distinguishedName, out ParsedDistinguishedName parsed) && IsOrganizationalUnit(parsed); + + internal static bool IsOrganizationalUnit(ParsedDistinguishedName parsed) => + parsed.Rdns.Count > 0 && parsed.Rdns[0].Attributes.Count == 1 && + string.Equals(parsed.Rdns[0].Attributes[0].Type, "OU", StringComparison.OrdinalIgnoreCase); + + /// Detects duplicate saved OUs only; live directory GUIDs remain authoritative. internal static string GetComparisonKey(ParsedDistinguishedName parsed) => string.Join("|", parsed.Rdns.Select(rdn => string.Join("+", rdn.Attributes.Select(attribute => $"{attribute.Type.ToUpperInvariant()}:{Convert.ToHexString(Encoding.UTF8.GetBytes(attribute.Value.ToUpperInvariant()))}").Order(StringComparer.Ordinal)))); diff --git a/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs b/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs index 224ef900f..4425a2064 100644 --- a/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs +++ b/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs @@ -47,7 +47,8 @@ public static DomainJoinValidationResult ValidateMetadata(DomainJoinSettings set if (issueId is null) Add(DomainJoinValidationCode.InvalidOuId); else if (!ids.Add(unit.Id)) Add(DomainJoinValidationCode.DuplicateOuId, issueId); if (!IsRequiredBounded(unit.DisplayName, 120)) Add(DomainJoinValidationCode.InvalidOuDisplayName, issueId); - if (!DistinguishedNameRules.TryParse(unit.DistinguishedName, out ParsedDistinguishedName parsed)) + if (!DistinguishedNameRules.TryParse(unit.DistinguishedName, out ParsedDistinguishedName parsed) || + !DistinguishedNameRules.IsOrganizationalUnit(parsed)) Add(DomainJoinValidationCode.InvalidDistinguishedName, issueId); else { diff --git a/src/Foundry.Deploy.Tests/DomainJoinStepViewModelTests.cs b/src/Foundry.Deploy.Tests/DomainJoinStepViewModelTests.cs index 851300e9d..0cf363a03 100644 --- a/src/Foundry.Deploy.Tests/DomainJoinStepViewModelTests.cs +++ b/src/Foundry.Deploy.Tests/DomainJoinStepViewModelTests.cs @@ -85,6 +85,9 @@ public void ChangingTheDomainReplacesTheSavedListWithATypedOu() step.TypedOuDistinguishedName = "OU=Sales,DC=corp,DC=test"; Assert.True(step.IsTypedOuInvalid); + step.TypedOuDistinguishedName = "CN=Computers,DC=other,DC=test"; + Assert.True(step.IsTypedOuInvalid); + step.TypedOuDistinguishedName = "OU=Field,DC=other,DC=test"; Assert.False(step.IsTypedOuInvalid); Assert.Equal("OU=Field,DC=other,DC=test", step.EffectiveOuDistinguishedName); diff --git a/src/Foundry.Deploy/Services/DomainJoin/DomainJoinPreparedInput.cs b/src/Foundry.Deploy/Services/DomainJoin/DomainJoinPreparedInput.cs index 1e2f0bfee..4fd1df968 100644 --- a/src/Foundry.Deploy/Services/DomainJoin/DomainJoinPreparedInput.cs +++ b/src/Foundry.Deploy/Services/DomainJoin/DomainJoinPreparedInput.cs @@ -21,7 +21,8 @@ public DomainJoinPreparedInput(DomainJoinCredentialContext credentialContext, st if (!DomainJoinCredentialContext.IsValidDomainName(credentialContext.DomainName) || !DomainJoinConfigurationValidator.IsQualifiedAccount(credentialContext.AccountName) || !ComputerNameRules.IsValid(computerName) || - targetOuDn is not null && !DistinguishedNameRules.IsWithinDomain(targetOuDn, credentialContext.DomainName)) + targetOuDn is not null && !(DistinguishedNameRules.IsOrganizationalUnit(targetOuDn) && + DistinguishedNameRules.IsWithinDomain(targetOuDn, credentialContext.DomainName))) throw new ArgumentException("The prepared domain input is invalid."); byte[] validated = DomainJoinCredentialPayloadCodec.Encode(credentialContext, password); CryptographicOperations.ZeroMemory(validated); diff --git a/src/Foundry.Deploy/ViewModels/DomainJoinStepViewModel.cs b/src/Foundry.Deploy/ViewModels/DomainJoinStepViewModel.cs index e5aefe1bf..318269566 100644 --- a/src/Foundry.Deploy/ViewModels/DomainJoinStepViewModel.cs +++ b/src/Foundry.Deploy/ViewModels/DomainJoinStepViewModel.cs @@ -64,7 +64,8 @@ public sealed partial class DomainJoinStepViewModel : ObservableObject, IDisposa public bool IsAccountInvalid => AccountName.Length > 0 && !DomainJoinConfigurationValidator.IsQualifiedAccount(AccountName); - public bool IsTypedOuInvalid => IsTypedOuVisible && TypedOu.Length > 0 && !DistinguishedNameRules.IsWithinDomain(TypedOu, DomainName); + public bool IsTypedOuInvalid => IsTypedOuVisible && TypedOu.Length > 0 && + !(DistinguishedNameRules.IsOrganizationalUnit(TypedOu) && DistinguishedNameRules.IsWithinDomain(TypedOu, DomainName)); /// Gets whether the wizard may leave the step and start a deployment with these inputs. public bool IsValid => !HasInput || diff --git a/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs b/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs index 3cdaccd4f..fafbb4ced 100644 --- a/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs +++ b/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs @@ -73,6 +73,20 @@ public async Task IncompleteReadiness_NeverJoins() Assert.Equal(0, f.Native.Joins); Assert.Equal(1, f.Directory.Prepares); } + [Fact] + public async Task MissingTargetOu_JoinsInTheDefaultLocationAndReportsPlacementFailure() + { + using var f = new DomainFixture(); + f.Directory.MissingTargetOu = true; + var result = await f.Run(); + Assert.Equal(DomainJoinPhaseState.Succeeded, result.Join.State); + Assert.True(result.RestartRequired); + Assert.Null(f.Native.CreationOu); + Assert.Equal(DomainJoinPhaseState.Failed, result.Placement.State); + Assert.Equal(DomainJoinFailureCode.OrganizationalUnitNotFound, result.Placement.FailureCode); + Assert.Equal(0, f.Directory.Moves); + } + [Fact] public async Task UnreachableDirectory_IsRetriedUntilReadyThenJoins() { @@ -294,6 +308,7 @@ internal sealed class FakeDirectory : IDomainComputerAccountDirectory public DomainDirectoryObject? Existing; public Guid? NewParent; public bool ReadinessFailure; + public bool MissingTargetOu; public int TransientFailures; public bool DenyReadback; public bool LoseMoveResponse; @@ -309,6 +324,7 @@ public Task PrepareAsync(DomainJoinCredentialContext conte Prepares++; ObservedPassword = password; AfterPrepare?.Invoke(); if (Prepares <= TransientFailures) throw new DomainDirectoryException(nativeError: 1355, transient: true); if (ReadinessFailure) throw new DomainDirectoryException(ldapError: 50); + if (MissingTargetOu && targetOuDn is not null) return Task.FromResult(new DomainDirectoryReady("example.test", "dc.example.test", null, null, DestinationMissing: true)); return Task.FromResult(new DomainDirectoryReady("example.test", "dc.example.test", targetOuDn is null ? null : Target, Existing)); } public Task FindComputerAsync(string name, CancellationToken token) => Task.FromResult(Computer with { ParentGuid = NewParent ?? Target.Guid }); diff --git a/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs b/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs index 91cc3ce7b..babc4ae60 100644 --- a/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs +++ b/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs @@ -40,11 +40,16 @@ public void DomainOutcomes_ShowIndependentPhasesAndPlacementWarning() [InlineData(DomainJoinPhaseState.Succeeded, DomainJoinPhaseState.Unverified, "Domain joined; target OU placement not confirmed")] [InlineData(DomainJoinPhaseState.Succeeded, DomainJoinPhaseState.Unknown, "Domain joined; target OU placement not confirmed")] [InlineData(DomainJoinPhaseState.Unknown, DomainJoinPhaseState.Skipped, "Domain join outcome unknown; membership is checked after restart")] + [InlineData(DomainJoinPhaseState.Succeeded, DomainJoinPhaseState.Failed, "Domain joined in the default location; target OU not found")] public void DomainOutcomes_ExplainUnconfirmedStates(DomainJoinPhaseState join, DomainJoinPhaseState placement, string expected) { using var f = new DomainFixture(); var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); report.Seed(); - DomainJoinResult result = report.Read() with { Join = new() { State = join }, Placement = new() { State = placement } }; + DomainJoinResult result = report.Read() with + { + Join = new() { State = join }, + Placement = new() { State = placement, FailureCode = placement == DomainJoinPhaseState.Failed ? DomainJoinFailureCode.OrganizationalUnitNotFound : null } + }; using var output = new StringWriter(); using var console = new PostInstallConsole(f.Plan, "test.log", output); console.Report(new([], "Running", DomainResult: result, WarningCount: 1)); diff --git a/src/Foundry.PostInstall/Console/PostInstallConsole.cs b/src/Foundry.PostInstall/Console/PostInstallConsole.cs index f9e050ac4..2d84b0aae 100644 --- a/src/Foundry.PostInstall/Console/PostInstallConsole.cs +++ b/src/Foundry.PostInstall/Console/PostInstallConsole.cs @@ -184,6 +184,8 @@ private string DomainText() => current.DomainResult is not { } domain ? string.E DomainJoinPhaseState.Unknown => "Domain join outcome unknown; membership is checked after restart", DomainJoinPhaseState.Succeeded => domain.Placement.State switch { + DomainJoinPhaseState.Failed when domain.Placement.FailureCode == DomainJoinFailureCode.OrganizationalUnitNotFound => + "Domain joined in the default location; target OU not found", DomainJoinPhaseState.Failed => "Domain joined; target OU placement failed", DomainJoinPhaseState.Unverified or DomainJoinPhaseState.Unknown => "Domain joined; target OU placement not confirmed", _ => null diff --git a/src/Foundry.PostInstall/Windows/DomainComputerAccountDirectory.cs b/src/Foundry.PostInstall/Windows/DomainComputerAccountDirectory.cs index cf26968de..311cc432e 100644 --- a/src/Foundry.PostInstall/Windows/DomainComputerAccountDirectory.cs +++ b/src/Foundry.PostInstall/Windows/DomainComputerAccountDirectory.cs @@ -22,8 +22,13 @@ Task PrepareAsync(DomainJoinCredentialContext context, Rea } /// Directory identity captured on the chosen DC, including the live parent's identity for computers. internal sealed record DomainDirectoryObject(Guid Guid, string DistinguishedName, Guid? ParentGuid = null); -/// A completed readiness read; null computer means no visible match, never proof of absolute absence. -internal sealed record DomainDirectoryReady(string Domain, string Controller, DomainDirectoryObject? Destination, DomainDirectoryObject? Computer); +/// +/// A completed readiness read; null computer means no visible match, never proof of absolute absence. +/// DestinationMissing means the directory answered that the requested OU does not exist or is not an +/// organizational unit, so the join can still proceed in the domain's default location. +/// +internal sealed record DomainDirectoryReady(string Domain, string Controller, DomainDirectoryObject? Destination, DomainDirectoryObject? Computer, + bool DestinationMissing = false); /// /// Sanitized numeric diagnostics; Rejected indicates an acknowledged server rejection of a write, and Transient a /// readiness failure that happened before the supplied identity was evaluated: the controller could not be located @@ -45,6 +50,7 @@ internal sealed class DomainComputerAccountDirectory : IDomainComputerAccountDir private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(10); private const int LdapServerDown = 81; private const int LdapConnectError = 91; + private const int NoSuchObject = 32; private const uint ForceRediscovery = 0x1; private LdapConnection? connection; private string domain = string.Empty; @@ -113,10 +119,26 @@ private async Task ConnectAsync(DomainJoinCredentialContex !DistinguishedNameRules.IsWithinDomain(namingContext, domain)) throw new DomainDirectoryException(); if (targetOuDn is null) return new(domain, controller, null, null); if (!DistinguishedNameRules.IsWithinDomain(targetOuDn, domain)) throw new DomainDirectoryException(); - var target = await ReadBaseAsync(targetOuDn, "(objectClass=organizationalUnit)", token).ConfigureAwait(false); + var target = await FindOrganizationalUnitAsync(targetOuDn, token).ConfigureAwait(false); + if (target is null) return new(domain, controller, null, null, DestinationMissing: true); return new(domain, controller, target, await FindComputerAsync(computerName, token).ConfigureAwait(false)); } + /// Returns null only when the directory positively answers that no such organizational unit exists. + private async Task FindOrganizationalUnitAsync(string dn, CancellationToken token) + { + try + { + var results = await SearchAsync(dn, "(objectClass=organizationalUnit)", SearchScope.Base, ["distinguishedName", "objectGUID"], token).ConfigureAwait(false); + if (results.Count > 1) throw new DomainDirectoryException(); + return results.Count == 0 ? null : Parse(results[0]); + } + catch (DomainDirectoryException error) when (error.DirectoryResultCode == NoSuchObject) + { + return null; + } + } + public async Task FindComputerAsync(string name, CancellationToken token) { var results = await SearchAsync(namingContext, "(&(objectCategory=computer)(sAMAccountName=" + EscapeFilter(name + "$") + "))", diff --git a/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs b/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs index 11ebfa734..8be09438a 100644 --- a/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs +++ b/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs @@ -7,6 +7,7 @@ using Foundry.Core.Models.PreOobe; using Foundry.Core.Services.Configuration; using Foundry.PostInstall.Execution; +using Serilog; namespace Foundry.PostInstall.Windows; @@ -38,6 +39,7 @@ public async Task RunAsync(DomainJoinActionParameters pa var store = new DomainJoinPhaseStore(root, plan, planHash); using var lease = store.AcquireWorkerLease(); var receipt = store.BindOrigin(originatingBoot); + Log.Information("Domain join worker started"); using (directory) using (var budget = CancellationTokenSource.CreateLinkedTokenSource(token)) { @@ -96,7 +98,9 @@ public async Task RunAsync(DomainJoinActionParameters pa receipt = receipt with { Join = join, RestartRequired = join.State == DomainJoinPhaseState.Succeeded }; Save(DomainJoinReceiptPhase.JoinReturned); if (join.State != DomainJoinPhaseState.Succeeded) return Finish(join, Skipped()); - if (ready.Destination is null) return Finish(join, Skipped()); + // A missing OU does not prevent the join: the account stays in the default location and placement is reported failed. + if (ready.Destination is null) + return Finish(join, ready.DestinationMissing ? Failure(DomainJoinPhaseState.Failed, DomainJoinFailureCode.OrganizationalUnitNotFound) : Skipped()); DomainJoinPhaseResult placement; try { @@ -152,6 +156,8 @@ DomainJoinWorkerResult Finish(DomainJoinPhaseResult join, DomainJoinPhaseResult { receipt = receipt with { Join = join, Placement = placement }; Save(DomainJoinReceiptPhase.Finished); + Log.Information("Domain join worker finished; join {JoinState} ({JoinFailure}), placement {PlacementState} ({PlacementFailure})", + join.State, join.FailureCode, placement.State, placement.FailureCode); return new(receipt.Join, receipt.Placement, receipt.ComputerObjectGuid, receipt.RestartRequired); } } @@ -163,7 +169,7 @@ DomainJoinWorkerResult Finish(DomainJoinPhaseResult join, DomainJoinPhaseResult private async Task WaitForDirectoryAsync(DomainJoinCredentialPayload credentials, DomainJoinActionParameters parameters, CancellationToken token) { - while (true) + for (int attempt = 1; ; attempt++) { try { @@ -173,6 +179,8 @@ private async Task WaitForDirectoryAsync(DomainJoinCredent catch (DomainDirectoryException error) when (error.Transient) { lastReadinessFailure = error; + Log.Warning("Domain controller not reachable yet; attempt {Attempt}, native {NativeErrorCode}, LDAP {LdapErrorCode}", + attempt, error.NativeErrorCode, error.LdapErrorCode); await delay(ReadinessRetryInterval, token).ConfigureAwait(false); } } @@ -195,7 +203,9 @@ private async Task ReadBackAsync(Guid computer, Guid target, CancellationT private static void ValidateReady(DomainDirectoryReady ready, DomainJoinActionParameters parameters) { if (DomainJoinCredentialContext.CanonicalizeDomainName(ready.Domain) != DomainJoinCredentialContext.CanonicalizeDomainName(parameters.DomainName) || - !DomainJoinCredentialContext.IsValidDomainName(ready.Controller) || (parameters.TargetOuDn is null) != (ready.Destination is null)) + !DomainJoinCredentialContext.IsValidDomainName(ready.Controller) || + ready.DestinationMissing && (parameters.TargetOuDn is null || ready.Destination is not null) || + !ready.DestinationMissing && (parameters.TargetOuDn is null) != (ready.Destination is null)) throw new InvalidDataException("Directory readiness is invalid."); if (ready.Destination is not null) ValidateObject(ready.Destination, parameters.DomainName); if (ready.Computer is not null) ValidateObject(ready.Computer, parameters.DomainName); From bc6fbe8d19dbebe497b69e156f8000510f68f430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Mon, 5 Oct 2026 22:25:59 +0200 Subject: [PATCH 40/84] refactor(domain-join): remove unused members and share the state seeds Build the staged phase receipt and result from one Core factory so Deploy and PostInstall can no longer drift apart, and drop the store methods that only tests called. Remove failure codes and a cleanup state nothing produces, the unread OU id on validation issues, the unread naming context on discovery results and an unused return value. Drop an overview branch for a combination the configuration cannot hold and a duplicate secret-state update. Align the last comments and one message with the OU vocabulary. Co-Authored-By: Claude Opus 5.5 --- .../FoundryConfigurationDocument.cs | 2 +- .../Models/PreOobe/DomainJoinPhaseReceipt.cs | 9 +++++++++ .../Models/PreOobe/DomainJoinResult.cs | 19 +++++++++++++++--- .../ConfigurationOverviewEvaluator.cs | 6 +----- .../DomainJoinConfigurationValidator.cs | 19 +++++++++--------- .../PreOobe/PreOobeTargetStagingService.cs | 20 ++++++------------- .../DomainJoin/DomainJoinDeploymentIntent.cs | 2 +- .../DomainJoinActionTests.cs | 6 +++--- .../DomainJoinRecoveryTests.cs | 2 +- .../DomainJoinResultStoreTests.cs | 10 +++++----- .../DomainJoinWorkerTests.cs | 12 ++++++++++- .../PostInstallConsoleTests.cs | 4 ++-- .../Execution/DomainJoinBinding.cs | 3 +-- .../Execution/DomainJoinPhaseStore.cs | 13 +++--------- .../Execution/DomainJoinResultStore.cs | 15 +++----------- .../Windows/DomainJoinWorker.cs | 2 +- .../DeploymentProfileSessionService.cs | 1 - .../AuthoringDomainOuDiscoveryService.cs | 20 +++++++++---------- .../IAuthoringDomainOuDiscoveryService.cs | 2 +- 19 files changed, 84 insertions(+), 83 deletions(-) diff --git a/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs b/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs index 2a3c509ef..4c66df598 100644 --- a/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs +++ b/src/Foundry.Core/Models/Configuration/FoundryConfigurationDocument.cs @@ -62,7 +62,7 @@ public sealed record FoundryConfigurationDocument /// public AutopilotSettings Autopilot { get; init; } = new(); - /// Gets optional online domain joining and its secret-free OU catalog. + /// Gets optional online domain joining and its secret-free OU list. public DomainJoinSettings DomainJoin { get; init; } = new(); /// diff --git a/src/Foundry.Core/Models/PreOobe/DomainJoinPhaseReceipt.cs b/src/Foundry.Core/Models/PreOobe/DomainJoinPhaseReceipt.cs index 9f75dc107..efbc59f07 100644 --- a/src/Foundry.Core/Models/PreOobe/DomainJoinPhaseReceipt.cs +++ b/src/Foundry.Core/Models/PreOobe/DomainJoinPhaseReceipt.cs @@ -38,4 +38,13 @@ public sealed record DomainJoinPhaseReceipt public Guid? DestinationObjectGuid { get; init; } [JsonRequired] public bool RestartRequired { get; init; } + + /// Creates the untouched receipt Deploy stages; the worker binds the installed boot before any mutation. + public static DomainJoinPhaseReceipt CreateSeed(string operationId, string attemptId, string planHash, string actionId) => new() + { + OperationId = operationId, + AttemptId = attemptId, + PlanHash = planHash, + ActionId = actionId + }; } diff --git a/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs b/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs index 0aded594c..b62760a69 100644 --- a/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs +++ b/src/Foundry.Core/Models/PreOobe/DomainJoinResult.cs @@ -13,14 +13,14 @@ public enum DomainJoinPhaseState { NotStarted, Succeeded, Failed, Skipped, Unkno public enum DomainJoinRestartState { NotRequired, Required, Requested, Completed } /// Tracks sensitive payload disposal independently from domain mutation outcomes. -public enum DomainJoinCleanupState { NotRequired, Pending, Disposed } +public enum DomainJoinCleanupState { Pending, Disposed } /// Allowlisted domain failures; diagnostics never persist account names or exception text. public enum DomainJoinFailureCode { - InvalidInput, CredentialUnavailable, ContextMismatch, ComputerNameMismatch, DomainUnavailable, + CredentialUnavailable, ContextMismatch, ComputerNameMismatch, DomainUnavailable, ReadinessTimeout, JoinFailed, PlacementFailed, MembershipMismatch, MembershipUnverified, - Interrupted, WorkerTimeout, CleanupFailed, ResultUnavailable, InvalidResult, OrganizationalUnitNotFound + Interrupted, WorkerTimeout, OrganizationalUnitNotFound } /// Reports one independent phase and an optional native or LDAP numeric diagnostic. @@ -67,4 +67,17 @@ public sealed record DomainJoinResult public DomainJoinCleanupState Cleanup { get; init; } [JsonRequired] public Guid? ComputerObjectGuid { get; init; } + + /// Creates the not-started result Deploy stages and PostInstall validates every later result against. + public static DomainJoinResult CreateSeed(string operationId, string attemptId, string planHash, + string computerName, string domainName, string? targetOuDn) => new() + { + OperationId = operationId, + AttemptId = attemptId, + PlanHash = planHash, + ExpectedComputerName = computerName, + ExpectedDomainName = domainName, + TargetOuDn = targetOuDn, + Cleanup = DomainJoinCleanupState.Pending + }; } diff --git a/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs b/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs index 4d11bbbbf..a13ce921b 100644 --- a/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs +++ b/src/Foundry.Core/Services/Configuration/ConfigurationOverviewEvaluator.cs @@ -212,11 +212,7 @@ public static ConfigurationOverviewEvaluation Evaluate(ConfigurationOverviewCont }; AddAutopilotStates(states, configuration.Autopilot, context.IsAutopilotConfigurationReady); - AddDomainJoinStates(states, configuration.DomainJoin, context.IsDomainJoinConfigurationReady && !configuration.Autopilot.IsEnabled); - if (configuration.Autopilot.IsEnabled && configuration.DomainJoin.IsEnabled) - { - AddAutopilotStates(states, configuration.Autopilot, false); - } + AddDomainJoinStates(states, configuration.DomainJoin, context.IsDomainJoinConfigurationReady); return new ConfigurationOverviewEvaluation(states); } diff --git a/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs b/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs index 4425a2064..d9dc194b2 100644 --- a/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs +++ b/src/Foundry.Core/Services/Configuration/DomainJoinConfigurationValidator.cs @@ -43,25 +43,24 @@ public static DomainJoinValidationResult ValidateMetadata(DomainJoinSettings set foreach (DomainJoinOrganizationalUnitSettings unit in units.Take(MaximumOrganizationalUnits)) { if (unit is null) { Add(DomainJoinValidationCode.InvalidOuId); continue; } - string? issueId = IsRequiredBounded(unit.Id, 128) ? unit.Id : null; - if (issueId is null) Add(DomainJoinValidationCode.InvalidOuId); - else if (!ids.Add(unit.Id)) Add(DomainJoinValidationCode.DuplicateOuId, issueId); - if (!IsRequiredBounded(unit.DisplayName, 120)) Add(DomainJoinValidationCode.InvalidOuDisplayName, issueId); + if (!IsRequiredBounded(unit.Id, 128)) Add(DomainJoinValidationCode.InvalidOuId); + else if (!ids.Add(unit.Id)) Add(DomainJoinValidationCode.DuplicateOuId); + if (!IsRequiredBounded(unit.DisplayName, 120)) Add(DomainJoinValidationCode.InvalidOuDisplayName); if (!DistinguishedNameRules.TryParse(unit.DistinguishedName, out ParsedDistinguishedName parsed) || !DistinguishedNameRules.IsOrganizationalUnit(parsed)) - Add(DomainJoinValidationCode.InvalidDistinguishedName, issueId); + Add(DomainJoinValidationCode.InvalidDistinguishedName); else { - if (!names.Add(DistinguishedNameRules.GetComparisonKey(parsed))) Add(DomainJoinValidationCode.DuplicateDistinguishedName, issueId); + if (!names.Add(DistinguishedNameRules.GetComparisonKey(parsed))) Add(DomainJoinValidationCode.DuplicateDistinguishedName); if (!DistinguishedNameRules.IsWithinDomain(unit.DistinguishedName, settings.OuCatalogDomain ?? string.Empty)) - Add(DomainJoinValidationCode.OuOutsideDomain, issueId); + Add(DomainJoinValidationCode.OuOutsideDomain); } } if (settings.DefaultOuId is not null && !ids.Contains(settings.DefaultOuId)) Add(DomainJoinValidationCode.DefaultOuMissing); return new(issues.Count == 0, issues); - void Add(DomainJoinValidationCode code, string? id = null) => issues.Add(new(code, id)); + void Add(DomainJoinValidationCode code) => issues.Add(new(code)); } /// Requires automatic credential inputs only when that mode is active; interactive media needs no password. @@ -114,8 +113,8 @@ public static bool IsQualifiedAccount(string? value) /// Contains non-secret validation outcomes. public sealed record DomainJoinValidationResult(bool IsValid, IReadOnlyList Issues); -/// Identifies the rule and optional authored OU that need attention without echoing input. -public sealed record DomainJoinValidationIssue(DomainJoinValidationCode Code, string? OuId = null); +/// Identifies the rule that needs attention without echoing input. +public sealed record DomainJoinValidationIssue(DomainJoinValidationCode Code); /// Stable validation codes for authoring and runtime presentation. public enum DomainJoinValidationCode diff --git a/src/Foundry.Deploy/Services/Deployment/PreOobe/PreOobeTargetStagingService.cs b/src/Foundry.Deploy/Services/Deployment/PreOobe/PreOobeTargetStagingService.cs index 16dbb0eb8..0b121a948 100644 --- a/src/Foundry.Deploy/Services/Deployment/PreOobe/PreOobeTargetStagingService.cs +++ b/src/Foundry.Deploy/Services/Deployment/PreOobe/PreOobeTargetStagingService.cs @@ -21,6 +21,7 @@ namespace Foundry.Deploy.Services.Deployment.PreOobe; /// Publishes verified local inputs and initial state before enabling the installed-Windows launch hook. public sealed class PreOobeTargetStagingService { + private const string DomainJoinActionId = "domain-join"; private readonly Action _protectDirectory; internal Action? BeforeHookPublication { get; init; } public PreOobeTargetStagingService() : this(ProtectDirectory) { } @@ -113,11 +114,11 @@ await DomainJoinRuntimeEligibility.SkipAsync(context, DomainJoinExecutionStatus. string destination = Path.Combine(layout.Root, relative.Replace('/', Path.DirectorySeparatorChar)); _protectDirectory(Path.GetDirectoryName(destination)!); domainCredentialPath = destination; - AddBuiltIn("domain-join", PreOobeBuiltInKind.DomainJoinAndPlacement, + AddBuiltIn(DomainJoinActionId, PreOobeBuiltInKind.DomainJoinAndPlacement, new DomainJoinActionParameters(intent.DomainName, intent.ComputerName, intent.TargetOuDn, relative)); AddBuiltIn("verify-domain-membership", PreOobeBuiltInKind.VerifyDomainMembership, - new DomainMembershipVerificationParameters(intent.DomainName, intent.ComputerName, "domain-join")); - owned.Add(new() { RelativePath = relative, IsSensitive = true, ConsumerActionIds = ["domain-join"] }); + new DomainMembershipVerificationParameters(intent.DomainName, intent.ComputerName, DomainJoinActionId)); + owned.Add(new() { RelativePath = relative, IsSensitive = true, ConsumerActionIds = [DomainJoinActionId] }); } var ai = context.Request.AiComponentRemoval; string[] aiNames = ai.IsEnabled ? new[] { ai.RemoveCopilot ? "Microsoft.Copilot" : null, ai.RemoveAiHub ? "Microsoft.Windows.AIHub" : null }.OfType().ToArray() : []; @@ -194,17 +195,8 @@ await DomainJoinRuntimeEligibility.SkipAsync(context, DomainJoinExecutionStatus. WriteIndented = true, Converters = { new JsonStringEnumConverter() } }; - var phase = new DomainJoinPhaseReceipt { OperationId = operationId, AttemptId = plan.AttemptId, PlanHash = hash, ActionId = "domain-join" }; - var result = new DomainJoinResult - { - OperationId = operationId, - AttemptId = plan.AttemptId, - PlanHash = hash, - ExpectedComputerName = intent.ComputerName, - ExpectedDomainName = intent.DomainName, - TargetOuDn = intent.TargetOuDn, - Cleanup = DomainJoinCleanupState.Pending - }; + var phase = DomainJoinPhaseReceipt.CreateSeed(operationId, plan.AttemptId, hash, DomainJoinActionId); + var result = DomainJoinResult.CreateSeed(operationId, plan.AttemptId, hash, intent.ComputerName, intent.DomainName, intent.TargetOuDn); foreach (var seed in new[] { (Name: "domain-join-phase.json", Json: JsonSerializer.Serialize(phase, options)), diff --git a/src/Foundry.Deploy/Services/DomainJoin/DomainJoinDeploymentIntent.cs b/src/Foundry.Deploy/Services/DomainJoin/DomainJoinDeploymentIntent.cs index 854a15272..b2418c98a 100644 --- a/src/Foundry.Deploy/Services/DomainJoin/DomainJoinDeploymentIntent.cs +++ b/src/Foundry.Deploy/Services/DomainJoin/DomainJoinDeploymentIntent.cs @@ -4,7 +4,7 @@ namespace Foundry.Deploy.Services.DomainJoin; -/// Freezes the non-secret destination and name selected before destructive confirmation. +/// Freezes the non-secret domain, computer name and OU selected before destructive confirmation. public sealed record DomainJoinDeploymentIntent(string DomainName, string ComputerName, string? TargetOuDn); /// Records that domain joining was requested even when an unsupported edition or simulation bypasses credentials. diff --git a/src/Foundry.PostInstall.Tests/DomainJoinActionTests.cs b/src/Foundry.PostInstall.Tests/DomainJoinActionTests.cs index 7cead77ac..ceab0b5ea 100644 --- a/src/Foundry.PostInstall.Tests/DomainJoinActionTests.cs +++ b/src/Foundry.PostInstall.Tests/DomainJoinActionTests.cs @@ -14,7 +14,7 @@ public async Task JoinAndFailedPlacement_StillRequestsRestartAndWritesSeparateRe { using var f = new DomainFixture(); f.Directory.Existing = f.Directory.Computer; f.Directory.StaleGuid = true; - new DomainJoinResultStore(f.Root, f.Plan, f.Hash).Seed(); + DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); var processes = new WorkerProcess(f); var result = await new DomainJoinAction(f.Root, f.Plan, f.Hash, "installed-boot", processes, "runtime.exe").ExecuteAsync(f.Plan.Actions[0], CancellationToken.None); Assert.False(result.Succeeded); Assert.True(result.RestartRequested); Assert.True(result.HasWarnings); @@ -29,7 +29,7 @@ public async Task JoinAndFailedPlacement_StillRequestsRestartAndWritesSeparateRe [Fact] public async Task TimeoutWithJoinStarted_ReportsUnknownAndCannotReplay() { - using var f = new DomainFixture(); new DomainJoinResultStore(f.Root, f.Plan, f.Hash).Seed(); + using var f = new DomainFixture(); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); var processes = new WorkerProcess(f) { Interrupt = true }; var action = new DomainJoinAction(f.Root, f.Plan, f.Hash, "installed-boot", processes, "runtime.exe"); var outcome = await action.ExecuteAsync(f.Plan.Actions[0], CancellationToken.None); @@ -45,7 +45,7 @@ public async Task TimeoutWithJoinStarted_ReportsUnknownAndCannotReplay() public async Task Verification_RequiresLaterBootAndExpectedActiveName(string boot, string name, bool success) { using var f = new DomainFixture(); - var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); report.Seed(); + var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); report.Write(report.Read() with { OriginatingBootId = "installed-boot", Join = new() { State = DomainJoinPhaseState.Succeeded }, Restart = DomainJoinRestartState.Required }); var journal = new ExecutionJournal(f.Root); var state = journal.Read(); state.Cursor = 1; state.BootIdentity = boot; state.Actions["verify"] = new() { Status = "Running" }; journal.Write(state); diff --git a/src/Foundry.PostInstall.Tests/DomainJoinRecoveryTests.cs b/src/Foundry.PostInstall.Tests/DomainJoinRecoveryTests.cs index e09733f11..06d7ca44c 100644 --- a/src/Foundry.PostInstall.Tests/DomainJoinRecoveryTests.cs +++ b/src/Foundry.PostInstall.Tests/DomainJoinRecoveryTests.cs @@ -444,7 +444,7 @@ public RecoveryFixture(string? fault = null, bool targetOu = true, bool continue }); Journal = fault is null ? new ExecutionJournal(Domain.Root) : new FaultJournal(Domain.Root, fault); Journal.Write(ExecutionJournal.CreateSeed(Domain.Plan, Domain.Hash)); - Report = new(Domain.Root, Domain.Plan, Domain.Hash); Report.Seed(); + Report = new(Domain.Root, Domain.Plan, Domain.Hash); DomainSeeds.WriteResult(Domain.Root, Domain.Plan, Domain.Hash); } public async Task Run(string currentBoot = "installed-boot") { diff --git a/src/Foundry.PostInstall.Tests/DomainJoinResultStoreTests.cs b/src/Foundry.PostInstall.Tests/DomainJoinResultStoreTests.cs index 18d0af110..b8ef92acf 100644 --- a/src/Foundry.PostInstall.Tests/DomainJoinResultStoreTests.cs +++ b/src/Foundry.PostInstall.Tests/DomainJoinResultStoreTests.cs @@ -28,7 +28,7 @@ public sealed class DomainJoinResultStoreTests public void MissingAuthoritativeReportField_RejectsDefaultSubstitution(string field) { using var f = new DomainFixture(target: false); - var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); store.Seed(); + var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); string path = Path.Combine(f.Root, "State", "PreOobe", "domain-join-result.json"); var value = JsonNode.Parse(File.ReadAllText(path))!.AsObject(); Assert.True(value.Remove(field)); @@ -43,7 +43,7 @@ public void MissingAuthoritativeReportField_RejectsDefaultSubstitution(string fi public void OptionalNumericReportDiagnostics_RemainCompatibleWhenOmitted(string field) { using var f = new DomainFixture(); - var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); store.Seed(); + var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); store.Write(store.Read() with { OriginatingBootId = "installed-boot", @@ -101,7 +101,7 @@ public void Receipt_RejectsWrongIdentityAndUnassignedMutation() public void Report_RejectsForeignIdentityAndSuccessfulJoinWithoutRestart() { using var f = new DomainFixture(); - var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); store.Seed(); + var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); var seed = store.Read(); Assert.Throws(() => store.Write(seed with { PlanHash = new string('b', 64) })); Assert.Throws(() => store.Write(seed with { OriginatingBootId = "installed-boot", Join = new() { State = DomainJoinPhaseState.Succeeded } })); @@ -109,7 +109,7 @@ public void Report_RejectsForeignIdentityAndSuccessfulJoinWithoutRestart() [Fact] public void Report_RejectsConflatedErrorFamilies() { - using var f = new DomainFixture(); var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); store.Seed(); + using var f = new DomainFixture(); var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); Assert.Throws(() => store.Write(store.Read() with { OriginatingBootId = "installed-boot", @@ -120,7 +120,7 @@ public void Report_RejectsConflatedErrorFamilies() public void Report_AcceptsRecordedOriginAfterRebootButNeverChangesIt() { using var f = new DomainFixture(); - var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); store.Seed(); + var store = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); store.Write(store.Read() with { OriginatingBootId = "installed-boot", Join = new() { State = DomainJoinPhaseState.Succeeded }, Restart = DomainJoinRestartState.Required }); var resumed = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); Assert.Equal("installed-boot", resumed.Read().OriginatingBootId); Assert.Throws(() => resumed.Write(resumed.Read() with { OriginatingBootId = "next-boot" })); diff --git a/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs b/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs index fafbb4ced..064e1f2e7 100644 --- a/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs +++ b/src/Foundry.PostInstall.Tests/DomainJoinWorkerTests.cs @@ -265,7 +265,7 @@ public DomainFixture(bool target = true, Func System.IO.Directory.Delete(Root, true); } +/// Writes the two state files as Deploy stages them, so tests start from the production seed shape. +internal static class DomainSeeds +{ + public static void WriteResult(string root, PreOobeExecutionPlan plan, string hash) => DomainStateFile.Write(root, + OwnedPaths.Resolve(root, "State/PreOobe/domain-join-result.json"), new DomainJoinResultStore(root, plan, hash).CreateSeed(), create: true); + + public static void WritePhase(string root, PreOobeExecutionPlan plan, string hash) => DomainStateFile.Write(root, + OwnedPaths.Resolve(root, "State/PreOobe/domain-join-phase.json"), new DomainJoinPhaseStore(root, plan, hash).CreateSeed(), create: true); +} + internal sealed class FakeNative : INativeDomainJoin { public string Name = "PC-01"; diff --git a/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs b/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs index babc4ae60..0306c247a 100644 --- a/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs +++ b/src/Foundry.PostInstall.Tests/PostInstallConsoleTests.cs @@ -14,7 +14,7 @@ public sealed class PostInstallConsoleTests public void DomainOutcomes_ShowIndependentPhasesAndPlacementWarning() { using var f = new DomainFixture(); - var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); report.Seed(); + var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); report.Write(report.Read() with { OriginatingBootId = "installed-boot", @@ -44,7 +44,7 @@ public void DomainOutcomes_ShowIndependentPhasesAndPlacementWarning() public void DomainOutcomes_ExplainUnconfirmedStates(DomainJoinPhaseState join, DomainJoinPhaseState placement, string expected) { using var f = new DomainFixture(); - var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); report.Seed(); + var report = new DomainJoinResultStore(f.Root, f.Plan, f.Hash); DomainSeeds.WriteResult(f.Root, f.Plan, f.Hash); DomainJoinResult result = report.Read() with { Join = new() { State = join }, diff --git a/src/Foundry.PostInstall/Execution/DomainJoinBinding.cs b/src/Foundry.PostInstall/Execution/DomainJoinBinding.cs index db74c717e..55955ebc4 100644 --- a/src/Foundry.PostInstall/Execution/DomainJoinBinding.cs +++ b/src/Foundry.PostInstall/Execution/DomainJoinBinding.cs @@ -55,7 +55,7 @@ public static void ValidateBoot(string boot) throw new InvalidDataException("Boot identity is invalid."); } - public static JournalState RequireRunning(string root, PreOobeExecutionPlan plan, string hash, string boot, string actionId) + public static void RequireRunning(string root, PreOobeExecutionPlan plan, string hash, string boot, string actionId) { ValidateHash(hash); ValidateBoot(boot); @@ -68,7 +68,6 @@ public static JournalState RequireRunning(string root, PreOobeExecutionPlan plan state.UnsafePayloadBootIdentity == boot && !(binding.VerificationAction.Id == actionId && state.UnsafeActionId == binding.JoinAction.Id)) throw new InvalidDataException("The worker is not the active action."); - return state; } public void RequireAction(PreOobeExecutionAction action, bool verification = false) diff --git a/src/Foundry.PostInstall/Execution/DomainJoinPhaseStore.cs b/src/Foundry.PostInstall/Execution/DomainJoinPhaseStore.cs index 445d3a2df..66242f1ab 100644 --- a/src/Foundry.PostInstall/Execution/DomainJoinPhaseStore.cs +++ b/src/Foundry.PostInstall/Execution/DomainJoinPhaseStore.cs @@ -12,16 +12,9 @@ namespace Foundry.PostInstall.Execution; internal sealed class DomainJoinPhaseStore(string root, PreOobeExecutionPlan plan, string planHash) { private readonly string path = OwnedPaths.Resolve(root, "State/PreOobe/domain-join-phase.json"); - /// Creates an unassigned WinPE seed; the installed boot is bound only by the worker. - public DomainJoinPhaseReceipt CreateSeed() => new() - { - OperationId = plan.OperationId, - AttemptId = plan.AttemptId, - PlanHash = planHash, - ActionId = DomainJoinBinding.Validate(plan).JoinAction.Id - }; - /// Creates a durable seed exactly once; never replaces an existing receipt. - public void Seed() { var seed = CreateSeed(); Validate(seed); DomainStateFile.Write(root, path, seed, create: true); } + /// Describes the unassigned receipt Deploy staged; the installed boot is bound only by the worker. + public DomainJoinPhaseReceipt CreateSeed() => + DomainJoinPhaseReceipt.CreateSeed(plan.OperationId, plan.AttemptId, planHash, DomainJoinBinding.Validate(plan).JoinAction.Id); /// Serializes domain workers independently of the parent-held runner lease. public IDisposable AcquireWorkerLease() => new FileStream(OwnedPaths.Resolve(root, "State/PreOobe/domain-worker.lease"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); public DomainJoinPhaseReceipt Read() diff --git a/src/Foundry.PostInstall/Execution/DomainJoinResultStore.cs b/src/Foundry.PostInstall/Execution/DomainJoinResultStore.cs index ba5a25a91..206742e8d 100644 --- a/src/Foundry.PostInstall/Execution/DomainJoinResultStore.cs +++ b/src/Foundry.PostInstall/Execution/DomainJoinResultStore.cs @@ -9,23 +9,14 @@ namespace Foundry.PostInstall.Execution; internal sealed class DomainJoinResultStore(string root, PreOobeExecutionPlan plan, string planHash) { private readonly string path = OwnedPaths.Resolve(root, "State/PreOobe/domain-join-result.json"); - /// Creates a credential-free report seed with no assumed installed-Windows boot. + /// Describes the credential-free report Deploy staged, with no assumed installed-Windows boot. public DomainJoinResult CreateSeed() { PreOobePlanValidator.ValidatePlan(plan); var binding = DomainJoinBinding.Validate(plan); - return new() - { - OperationId = plan.OperationId, - AttemptId = plan.AttemptId, - PlanHash = planHash, - ExpectedComputerName = binding.Parameters.ComputerName, - ExpectedDomainName = binding.Parameters.DomainName, - TargetOuDn = binding.Parameters.TargetOuDn, - Cleanup = DomainJoinCleanupState.Pending - }; + return DomainJoinResult.CreateSeed(plan.OperationId, plan.AttemptId, planHash, + binding.Parameters.ComputerName, binding.Parameters.DomainName, binding.Parameters.TargetOuDn); } - public void Seed() { var seed = CreateSeed(); Validate(seed); DomainStateFile.Write(root, path, seed, create: true); } public DomainJoinResult Read() { var value = DomainStateFile.Read(root, path); Validate(value); return value; diff --git a/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs b/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs index 8be09438a..636092764 100644 --- a/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs +++ b/src/Foundry.PostInstall/Windows/DomainJoinWorker.cs @@ -107,7 +107,7 @@ public async Task RunAsync(DomainJoinActionParameters pa budget.Token.ThrowIfCancellationRequested(); var target = await directory.ReadDestinationAsync(ready.Destination.Guid, budget.Token).ConfigureAwait(false); ValidateObject(target, parameters.DomainName); - if (target.Guid != ready.Destination.Guid) throw new InvalidDataException("The destination identity changed."); + if (target.Guid != ready.Destination.Guid) throw new InvalidDataException("The OU identity changed."); if (ready.Computer is null) { var created = await directory.FindComputerAsync(parameters.ComputerName, budget.Token).ConfigureAwait(false); diff --git a/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs b/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs index 8d00352e3..6576cd531 100644 --- a/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs +++ b/src/Foundry/Services/Configuration/DeploymentProfileSessionService.cs @@ -380,7 +380,6 @@ private void RememberSourceMetadata(DeploymentProfileDocument profile, FoundryCo private void OnConfigurationChanged(object? sender, EventArgs e) { - domainSecrets.Update(configurationState.Current.DomainJoin); if (sourceMetadata is null || materializedSourceMetadata is null) return; FoundryConfigurationDocument current = configurationState.Current with { diff --git a/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs b/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs index ce116ead4..ec7fcb9c2 100644 --- a/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs +++ b/src/Foundry/Services/DomainJoin/AuthoringDomainOuDiscoveryService.cs @@ -43,7 +43,7 @@ public async Task DiscoverAsync(CancellationToken cance } catch (OperationCanceledException) { - return result = new(null, null, [], cancellationToken.IsCancellationRequested ? DomainOuDiscoveryStatus.Canceled : DomainOuDiscoveryStatus.Unavailable, + return result = new(null, [], cancellationToken.IsCancellationRequested ? DomainOuDiscoveryStatus.Canceled : DomainOuDiscoveryStatus.Unavailable, cancellationToken.IsCancellationRequested ? "Canceled" : "Timeout"); } finally @@ -55,8 +55,8 @@ public async Task DiscoverAsync(CancellationToken cance } }, CancellationToken.None); try { return await work.WaitAsync(TimeSpan.FromSeconds(60), cancellationToken).ConfigureAwait(false); } - catch (TimeoutException) { return new(null, null, [], DomainOuDiscoveryStatus.Unavailable, "Timeout"); } - catch (OperationCanceledException) { return new(null, null, [], DomainOuDiscoveryStatus.Canceled, "Canceled"); } + catch (TimeoutException) { return new(null, [], DomainOuDiscoveryStatus.Unavailable, "Timeout"); } + catch (OperationCanceledException) { return new(null, [], DomainOuDiscoveryStatus.Canceled, "Canceled"); } } private static async Task DiscoverOwnedAsync(CancellationToken token) @@ -96,26 +96,26 @@ private static async Task DiscoverOwnedAsync(Cancellati bool valid = AddEntries(response, candidates, domain); PageResultResponseControl[] pages = response.Controls.OfType().ToArray(); if (!valid || response.References.Count > 0 || pages.Length != 1) - return new(domain, namingContext, candidates, DomainOuDiscoveryStatus.Incomplete, "Incomplete"); + return new(domain, candidates, DomainOuDiscoveryStatus.Incomplete, "Incomplete"); if (pages[0].Cookie.Length == 0) - return new(domain, namingContext, candidates, DomainOuDiscoveryStatus.Complete); + return new(domain, candidates, DomainOuDiscoveryStatus.Complete); if (candidates.Count >= 4096) - return new(domain, namingContext, candidates, DomainOuDiscoveryStatus.Incomplete, "Limit"); + return new(domain, candidates, DomainOuDiscoveryStatus.Incomplete, "Limit"); paging.Cookie = pages[0].Cookie; } } - catch (OperationCanceledException) { return new(domain, namingContext, [], DomainOuDiscoveryStatus.Canceled, "Canceled"); } + catch (OperationCanceledException) { return new(domain, [], DomainOuDiscoveryStatus.Canceled, "Canceled"); } catch (DirectoryOperationException ex) { if (ex.Response is SearchResponse partial && domain is not null) AddEntries(partial, candidates, domain); bool incomplete = candidates.Count > 0 || ex.Response?.ResultCode is ResultCode.TimeLimitExceeded or ResultCode.SizeLimitExceeded or ResultCode.Referral; - return new(domain, namingContext, candidates, incomplete ? DomainOuDiscoveryStatus.Incomplete : DomainOuDiscoveryStatus.Unavailable, + return new(domain, candidates, incomplete ? DomainOuDiscoveryStatus.Incomplete : DomainOuDiscoveryStatus.Unavailable, "DirectoryRead", DirectoryResultCode: ex.Response is null ? null : (int)ex.Response.ResultCode); } - catch (Win32Exception ex) { return new(domain, namingContext, [], DomainOuDiscoveryStatus.Unavailable, "ComputerDomainUnavailable", NativeErrorCode: ex.NativeErrorCode); } + catch (Win32Exception ex) { return new(domain, [], DomainOuDiscoveryStatus.Unavailable, "ComputerDomainUnavailable", NativeErrorCode: ex.NativeErrorCode); } catch (Exception ex) when (ex is LdapException or InvalidDataException or InvalidOperationException or ArgumentException or NotSupportedException) { - return new(domain, namingContext, candidates, candidates.Count > 0 ? DomainOuDiscoveryStatus.Incomplete : DomainOuDiscoveryStatus.Unavailable, + return new(domain, candidates, candidates.Count > 0 ? DomainOuDiscoveryStatus.Incomplete : DomainOuDiscoveryStatus.Unavailable, ex is InvalidOperationException ? "ComputerDomainUnavailable" : "DirectoryRead", LdapErrorCode: (ex as LdapException)?.ErrorCode); } } diff --git a/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs b/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs index 0c3da9aa2..df534ff34 100644 --- a/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs +++ b/src/Foundry/Services/DomainJoin/IAuthoringDomainOuDiscoveryService.cs @@ -17,6 +17,6 @@ public interface IAuthoringDomainOuDiscoveryService public enum DomainOuDiscoveryStatus { Complete, Incomplete, Unavailable, Canceled } /// Contains only portable directory metadata and an allowlisted presentation code. -public sealed record DomainOuDiscoveryResult(string? ComputerDomain, string? NamingContext, +public sealed record DomainOuDiscoveryResult(string? ComputerDomain, IReadOnlyList Candidates, DomainOuDiscoveryStatus Status, string? ErrorCode = null, int? NativeErrorCode = null, int? LdapErrorCode = null, int? DirectoryResultCode = null); From 5bc8a910a6d8389b10f2682da7c3190322643e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20CHAVE?= Date: Tue, 6 Oct 2026 00:24:28 +0200 Subject: [PATCH 41/84] refactor(domain-join): manage OUs like Autopilot profiles on the authoring pages Replace the expander that mixed inline entry, search results and the saved list with the layout of the Autopilot JSON profile page: an actions card, the default OU, the technician choice and the OU list. Add OU now opens a dialog that keeps the entry until it is accepted. Import from domain searches the computer's domain and opens a picker with per-row checkboxes, none selected by default; the same button cancels a running search. Search progress and failures show beside the actions. Co-Authored-By: Claude Opus 5.5 --- .../ServiceCollectionExtensions.cs | 1 + .../DomainJoin/DomainJoinOuDialogService.cs | 44 +++ .../DomainJoin/IDomainJoinOuDialogService.cs | 25 ++ src/Foundry/Strings/ar-SA/Resources.resw | 20 +- src/Foundry/Strings/bg-BG/Resources.resw | 20 +- src/Foundry/Strings/cs-CZ/Resources.resw | 20 +- src/Foundry/Strings/da-DK/Resources.resw | 20 +- src/Foundry/Strings/de-DE/Resources.resw | 20 +- src/Foundry/Strings/el-GR/Resources.resw | 20 +- src/Foundry/Strings/en-GB/Resources.resw | 20 +- src/Foundry/Strings/en-US/Resources.resw | 20 +- src/Foundry/Strings/es-ES/Resources.resw | 20 +- src/Foundry/Strings/es-MX/Resources.resw | 20 +- src/Foundry/Strings/et-EE/Resources.resw | 20 +- src/Foundry/Strings/fi-FI/Resources.resw | 20 +- src/Foundry/Strings/fr-CA/Resources.resw | 20 +- src/Foundry/Strings/fr-FR/Resources.resw | 20 +- src/Foundry/Strings/he-IL/Resources.resw | 20 +- src/Foundry/Strings/hr-HR/Resources.resw | 20 +- src/Foundry/Strings/hu-HU/Resources.resw | 20 +- src/Foundry/Strings/it-IT/Resources.resw | 20 +- src/Foundry/Strings/ja-JP/Resources.resw | 20 +- src/Foundry/Strings/ko-KR/Resources.resw | 20 +- src/Foundry/Strings/lt-LT/Resources.resw | 20 +- src/Foundry/Strings/lv-LV/Resources.resw | 20 +- src/Foundry/Strings/nb-NO/Resources.resw | 20 +- src/Foundry/Strings/nl-NL/Resources.resw | 20 +- src/Foundry/Strings/pl-PL/Resources.resw | 20 +- src/Foundry/Strings/pt-BR/Resources.resw | 20 +- src/Foundry/Strings/pt-PT/Resources.resw | 20 +- src/Foundry/Strings/ro-RO/Resources.resw | 20 +- src/Foundry/Strings/ru-RU/Resources.resw | 20 +- src/Foundry/Strings/sk-SK/Resources.resw | 20 +- src/Foundry/Strings/sl-SI/Resources.resw | 20 +- src/Foundry/Strings/sr-Latn-RS/Resources.resw | 20 +- src/Foundry/Strings/sv-SE/Resources.resw | 20 +- src/Foundry/Strings/th-TH/Resources.resw | 20 +- src/Foundry/Strings/tr-TR/Resources.resw | 20 +- src/Foundry/Strings/uk-UA/Resources.resw | 20 +- src/Foundry/Strings/zh-CN/Resources.resw | 20 +- src/Foundry/Strings/zh-TW/Resources.resw | 20 +- .../DomainJoinConfigurationViewModel.cs | 188 +++++------ .../DomainJoinOuSelectionDialogViewModel.cs | 113 +++++++ src/Foundry/Views/DomainJoinOuAddDialog.xaml | 27 ++ .../Views/DomainJoinOuAddDialog.xaml.cs | 32 ++ .../Views/DomainJoinOuSelectionDialog.xaml | 80 +++++ .../Views/DomainJoinOuSelectionDialog.xaml.cs | 80 +++++ .../Views/InteractiveDomainJoinPage.xaml | 299 +++++++----------- .../Views/InteractiveDomainJoinPage.xaml.cs | 7 - .../Views/ZeroTouchDomainJoinPage.xaml | 299 +++++++----------- .../Views/ZeroTouchDomainJoinPage.xaml.cs | 7 - 51 files changed, 1211 insertions(+), 751 deletions(-) create mode 100644 src/Foundry/Services/DomainJoin/DomainJoinOuDialogService.cs create mode 100644 src/Foundry/Services/DomainJoin/IDomainJoinOuDialogService.cs create mode 100644 src/Foundry/ViewModels/DomainJoinOuSelectionDialogViewModel.cs create mode 100644 src/Foundry/Views/DomainJoinOuAddDialog.xaml create mode 100644 src/Foundry/Views/DomainJoinOuAddDialog.xaml.cs create mode 100644 src/Foundry/Views/DomainJoinOuSelectionDialog.xaml create mode 100644 src/Foundry/Views/DomainJoinOuSelectionDialog.xaml.cs diff --git a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs index a9e64fe14..862910d2f 100644 --- a/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Foundry/DependencyInjection/ServiceCollectionExtensions.cs @@ -173,6 +173,7 @@ public static IServiceCollection AddFoundryApplicationServices(this IServiceColl services.AddTransient(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Foundry/Services/DomainJoin/DomainJoinOuDialogService.cs b/src/Foundry/Services/DomainJoin/DomainJoinOuDialogService.cs new file mode 100644 index 000000000..e3f052772 --- /dev/null +++ b/src/Foundry/Services/DomainJoin/DomainJoinOuDialogService.cs @@ -0,0 +1,44 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; +using Foundry.Services.Localization; + +namespace Foundry.Services.DomainJoin; + +public sealed class DomainJoinOuDialogService(IApplicationLocalizationService localization) : IDomainJoinOuDialogService +{ + public async Task ShowAddAsync(Func tryAdd) + { + ArgumentNullException.ThrowIfNull(tryAdd); + + var dialog = new DomainJoinOuAddDialog(localization, tryAdd) + { + XamlRoot = App.MainWindow.Content.XamlRoot + }; + await dialog.ShowAsync(); + } + + public async Task?> PickAsync( + IReadOnlyList candidates, bool isIncomplete) + { + ArgumentNullException.ThrowIfNull(candidates); + + var viewModel = new DomainJoinOuSelectionDialogViewModel(localization, candidates, isIncomplete); + var dialog = new DomainJoinOuSelectionDialog(viewModel) + { + XamlRoot = App.MainWindow.Content.XamlRoot + }; + + try + { + ContentDialogResult result = await dialog.ShowAsync(); + return result == ContentDialogResult.Primary ? viewModel.GetSelected() : null; + } + finally + { + viewModel.Dispose(); + } + } +} diff --git a/src/Foundry/Services/DomainJoin/IDomainJoinOuDialogService.cs b/src/Foundry/Services/DomainJoin/IDomainJoinOuDialogService.cs new file mode 100644 index 000000000..c420a6177 --- /dev/null +++ b/src/Foundry/Services/DomainJoin/IDomainJoinOuDialogService.cs @@ -0,0 +1,25 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Core.Models.Configuration; + +namespace Foundry.Services.DomainJoin; + +/// Shows the dialogs that add organizational units to the Domain Join list. +public interface IDomainJoinOuDialogService +{ + /// + /// Asks for one OU. The dialog stays open while returns a message, so the entry can + /// be corrected in place. + /// + /// Adds the display name and distinguished name; returns the refusal reason, or null. + Task ShowAddAsync(Func tryAdd); + + /// Lets the user choose which discovered OUs to add. + /// OUs found in the authoring computer's domain. + /// Whether the directory returned only part of its OUs. + /// The chosen OUs, or when the dialog is canceled. + Task?> PickAsync( + IReadOnlyList candidates, bool isIncomplete); +} diff --git a/src/Foundry/Strings/ar-SA/Resources.resw b/src/Foundry/Strings/ar-SA/Resources.resw index 3602907d6..c59193331 100644 --- a/src/Foundry/Strings/ar-SA/Resources.resw +++ b/src/Foundry/Strings/ar-SA/Resources.resw @@ -1840,8 +1840,8 @@ تم إلغاء البحث. - - اكتمل البحث. حدد وحدات OU المراد إضافتها. + + لم يتم العثور على أي وحدات OU في نطاق هذا الكمبيوتر. تعذّر عرض سوى بعض وحدات OU. لا يزال بإمكانك إضافة الوحدات المعروضة. @@ -1858,12 +1858,12 @@ لم تتم إضافة وحدات OU. إنها تنتمي إلى نطاق يختلف عن اسم النطاق أو عن وحدات OU المدرجة مسبقاً. - - تمت إضافة وحدات OU المحددة. - إزالة المحدد + + وحدات OU المحددة: {0} من {1} + أدخل الحساب. @@ -1961,7 +1961,7 @@ وحدة OU الافتراضية - البحث عن وحدات OU + استيراد من النطاق يعرض وحدات OU في النطاق المنضم إليه هذا الكمبيوتر باستخدام حساب Windows الخاص بك. حدد الوحدات المراد إضافتها. @@ -1976,7 +1976,7 @@ إضافة وحدات OU المحددة - أدخل اسم عرض والاسم المميز لوحدة OU. يجب أن تنتمي وحدة OU إلى النطاق أعلاه. + أدخل اسم عرض والاسم المميز لوحدة OU. يجب أن تنتمي وحدة OU إلى النطاق الذي تم تكوينه. إضافة OU يدوياً @@ -1990,6 +1990,12 @@ اسم العرض + + أضف وحدة OU يدوياً أو استورد وحدات OU من النطاق المنضم إليه هذا الكمبيوتر. + + + إجراءات وحدات OU + كلمة المرور diff --git a/src/Foundry/Strings/bg-BG/Resources.resw b/src/Foundry/Strings/bg-BG/Resources.resw index c22e663ba..2819dd8d0 100644 --- a/src/Foundry/Strings/bg-BG/Resources.resw +++ b/src/Foundry/Strings/bg-BG/Resources.resw @@ -1840,8 +1840,8 @@ Търсенето е отменено. - - Търсенето завърши. Изберете кои OU да добавите. + + Не бяха намерени OU в домейна на този компютър. Само част от OU можаха да бъдат изведени. Показаните все пак могат да бъдат добавени. @@ -1858,12 +1858,12 @@ OU не бяха добавени. Те принадлежат към домейн, различен от името на домейна или от вече изброените OU. - - Избраните OU бяха добавени. - Премахване на избраното + + Избрани OU: {0} от {1} + Въведете акаунта. @@ -1961,7 +1961,7 @@ OU по подразбиране - Търсене на OU + Импортиране от домейна Извежда OU от домейна, към който е присъединен този компютър, с вашия акаунт за Windows. Изберете кои да добавите. @@ -1976,7 +1976,7 @@ Добавяне на избраните OU - Въведете показвано име и отличителното име на OU. OU трябва да принадлежи към домейна по-горе. + Въведете показвано име и отличителното име на OU. OU трябва да принадлежи към конфигурирания домейн. Ръчно добавяне на OU @@ -1990,6 +1990,12 @@ Показвано име + + Добавете OU ръчно или импортирайте OU от домейна, към който е присъединен този компютър. + + + Действия за OU + Парола diff --git a/src/Foundry/Strings/cs-CZ/Resources.resw b/src/Foundry/Strings/cs-CZ/Resources.resw index 270fac537..0c9fe8191 100644 --- a/src/Foundry/Strings/cs-CZ/Resources.resw +++ b/src/Foundry/Strings/cs-CZ/Resources.resw @@ -1840,8 +1840,8 @@ Licencováno pod licencí MIT. Vyhledávání bylo zrušeno. - - Vyhledávání je dokončeno. Vyberte jednotky OU, které chcete přidat. + + V doméně tohoto počítače nebyly nalezeny žádné jednotky OU. Podařilo se vypsat jen některé jednotky OU. Zobrazené jednotky můžete přesto přidat. @@ -1858,12 +1858,12 @@ Licencováno pod licencí MIT. Jednotky OU nebyly přidány. Patří do jiné domény, než je název domény nebo než jsou již uvedené jednotky OU. - - Vybrané jednotky OU byly přidány. - Odebrat vybrané + + Vybrané jednotky OU: {0} z {1} + Zadejte účet. @@ -1961,7 +1961,7 @@ Licencováno pod licencí MIT. Výchozí OU - Najít OU + Importovat z domény Vypíše jednotky OU domény, ke které je tento počítač připojen, pomocí vašeho účtu Windows. Vyberte ty, které chcete přidat. @@ -1976,7 +1976,7 @@ Licencováno pod licencí MIT. Přidat vybrané OU - Zadejte zobrazovaný název a rozlišující název jednotky OU. Jednotka OU musí patřit do výše uvedené domény. + Zadejte zobrazovaný název a rozlišující název jednotky OU. Jednotka OU musí patřit do nakonfigurované domény. Přidat OU ručně @@ -1990,6 +1990,12 @@ Licencováno pod licencí MIT. Zobrazovaný název + + Přidejte jednotku OU ručně nebo importujte jednotky OU z domény, ke které je tento počítač připojen. + + + Akce OU + Heslo diff --git a/src/Foundry/Strings/da-DK/Resources.resw b/src/Foundry/Strings/da-DK/Resources.resw index 2f899868f..364a96304 100644 --- a/src/Foundry/Strings/da-DK/Resources.resw +++ b/src/Foundry/Strings/da-DK/Resources.resw @@ -1840,8 +1840,8 @@ Licenseret under MIT-licensen. Søgningen blev annulleret. - - Søgningen er fuldført. Vælg de OU'er, der skal tilføjes. + + Der blev ikke fundet nogen OU'er i denne computers domæne. Kun nogle af OU'erne kunne vises. Du kan stadig tilføje dem, der vises. @@ -1858,12 +1858,12 @@ Licenseret under MIT-licensen. OU'erne blev ikke tilføjet. De tilhører et andet domæne end domænenavnet eller de OU'er, der allerede er på listen. - - De valgte OU'er blev tilføjet. - Fjern valgte + + Valgte OU'er: {0} af {1} + Angiv kontoen. @@ -1961,7 +1961,7 @@ Licenseret under MIT-licensen. Standard-OU - Find OU'er + Importér fra domæne Viser OU'erne i det domæne, som denne computer er tilsluttet, ved hjælp af din Windows-konto. Vælg dem, der skal tilføjes. @@ -1976,7 +1976,7 @@ Licenseret under MIT-licensen. Tilføj valgte OU'er - Angiv et visningsnavn og OU'ens entydige navn. OU'en skal tilhøre domænet ovenfor. + Angiv et visningsnavn og OU'ens entydige navn. OU'en skal tilhøre det konfigurerede domæne. Tilføj en OU manuelt @@ -1990,6 +1990,12 @@ Licenseret under MIT-licensen. Visningsnavn + + Tilføj en OU manuelt, eller importér OU'er fra det domæne, som denne computer er tilsluttet. + + + OU-handlinger + Adgangskode diff --git a/src/Foundry/Strings/de-DE/Resources.resw b/src/Foundry/Strings/de-DE/Resources.resw index 77b219e3d..85b05168f 100644 --- a/src/Foundry/Strings/de-DE/Resources.resw +++ b/src/Foundry/Strings/de-DE/Resources.resw @@ -1840,8 +1840,8 @@ Lizenziert unter der MIT-Lizenz. Suche abgebrochen. - - Suche abgeschlossen. Wählen Sie die OUs aus, die hinzugefügt werden sollen. + + In der Domäne dieses Computers wurden keine OUs gefunden. Es konnten nur einige OUs aufgelistet werden. Die angezeigten OUs können Sie trotzdem hinzufügen. @@ -1858,12 +1858,12 @@ Lizenziert unter der MIT-Lizenz. Die OUs wurden nicht hinzugefügt. Sie gehören zu einer anderen Domäne als der Domänenname oder die bereits aufgelisteten OUs. - - Die ausgewählten OUs wurden hinzugefügt. - Ausgewählte entfernen + + Ausgewählte OUs: {0} von {1} + Geben Sie das Konto ein. @@ -1961,7 +1961,7 @@ Lizenziert unter der MIT-Lizenz. Standard-OU - OUs suchen + Aus Domäne importieren Listet mit Ihrem Windows-Konto die OUs der Domäne auf, der dieser Computer beigetreten ist. Wählen Sie die OUs aus, die hinzugefügt werden sollen. @@ -1976,7 +1976,7 @@ Lizenziert unter der MIT-Lizenz. Ausgewählte OUs hinzufügen - Geben Sie einen Anzeigenamen und den Distinguished Name der OU ein. Die OU muss zur oben angegebenen Domäne gehören. + Geben Sie einen Anzeigenamen und den Distinguished Name der OU ein. Die OU muss zur konfigurierten Domäne gehören. OU manuell hinzufügen @@ -1990,6 +1990,12 @@ Lizenziert unter der MIT-Lizenz. Anzeigename + + Fügen Sie eine OU manuell hinzu, oder importieren Sie OUs aus der Domäne, der dieser Computer beigetreten ist. + + + OU-Aktionen + Kennwort diff --git a/src/Foundry/Strings/el-GR/Resources.resw b/src/Foundry/Strings/el-GR/Resources.resw index 097634687..11d34ae86 100644 --- a/src/Foundry/Strings/el-GR/Resources.resw +++ b/src/Foundry/Strings/el-GR/Resources.resw @@ -1840,8 +1840,8 @@ Η αναζήτηση ακυρώθηκε. - - Η αναζήτηση ολοκληρώθηκε. Επιλέξτε τις OU που θα προστεθούν. + + Δεν βρέθηκαν OU στον τομέα αυτού του υπολογιστή. Ήταν δυνατή η εμφάνιση μόνο ορισμένων OU. Μπορείτε ωστόσο να προσθέσετε όσες εμφανίζονται. @@ -1858,12 +1858,12 @@ Οι OU δεν προστέθηκαν. Ανήκουν σε διαφορετικό τομέα από το όνομα τομέα ή από τις OU που υπάρχουν ήδη στη λίστα. - - Οι επιλεγμένες OU προστέθηκαν. - Κατάργηση επιλεγμένων + + Επιλεγμένες OU: {0} από {1} + Εισαγάγετε τον λογαριασμό. @@ -1961,7 +1961,7 @@ Προεπιλεγμένη OU - Εύρεση OU + Εισαγωγή από τομέα Εμφανίζει τις OU του τομέα στον οποίο είναι συνδεδεμένος αυτός ο υπολογιστής, με χρήση του λογαριασμού σας Windows. Επιλέξτε όσες θέλετε να προσθέσετε. @@ -1976,7 +1976,7 @@ Προσθήκη επιλεγμένων OU - Εισαγάγετε ένα εμφανιζόμενο όνομα και το διακεκριμένο όνομα της OU. Η OU πρέπει να ανήκει στον παραπάνω τομέα. + Εισαγάγετε ένα εμφανιζόμενο όνομα και το διακεκριμένο όνομα της OU. Η OU πρέπει να ανήκει στον διαμορφωμένο τομέα. Χειροκίνητη προσθήκη OU @@ -1990,6 +1990,12 @@ Εμφανιζόμενο όνομα + + Προσθέστε μια OU χειροκίνητα ή εισαγάγετε OU από τον τομέα στον οποίο είναι συνδεδεμένος αυτός ο υπολογιστής. + + + Ενέργειες OU + Κωδικός πρόσβασης diff --git a/src/Foundry/Strings/en-GB/Resources.resw b/src/Foundry/Strings/en-GB/Resources.resw index 164cbf4f0..def76f762 100644 --- a/src/Foundry/Strings/en-GB/Resources.resw +++ b/src/Foundry/Strings/en-GB/Resources.resw @@ -1840,8 +1840,8 @@ Licensed under the MIT License. Search cancelled. - - Search complete. Select the OUs to add. + + No OUs were found in this computer's domain. Only some of the OUs could be listed. You can still add the ones shown. @@ -1858,12 +1858,12 @@ Licensed under the MIT License. The OUs were not added. They belong to a different domain than the domain name or the OUs already listed. - - The selected OUs were added. - Remove selected + + Selected OUs: {0} of {1} + Enter the account. @@ -1961,7 +1961,7 @@ Licensed under the MIT License. Default OU - Find OUs + Import from domain Lists the OUs of the domain this computer is joined to, using your Windows account. Select the ones to add. @@ -1976,7 +1976,7 @@ Licensed under the MIT License. Add selected OUs - Enter a display name and the distinguished name of the OU. The OU must belong to the domain above. + Enter a display name and the distinguished name of the OU. The OU must belong to the configured domain. Add an OU manually @@ -1990,6 +1990,12 @@ Licensed under the MIT License. Display name + + Add an OU manually or import OUs from the domain this computer is joined to. + + + OU actions + Password diff --git a/src/Foundry/Strings/en-US/Resources.resw b/src/Foundry/Strings/en-US/Resources.resw index bb806be8d..2bdcabfa5 100644 --- a/src/Foundry/Strings/en-US/Resources.resw +++ b/src/Foundry/Strings/en-US/Resources.resw @@ -1840,8 +1840,8 @@ Licensed under the MIT License. Search canceled. - - Search complete. Select the OUs to add. + + No OUs were found in this computer's domain. Only some of the OUs could be listed. You can still add the ones shown. @@ -1858,12 +1858,12 @@ Licensed under the MIT License. The OUs were not added. They belong to a different domain than the domain name or the OUs already listed. - - The selected OUs were added. - Remove selected + + Selected OUs: {0} of {1} + Enter the account. @@ -1961,7 +1961,7 @@ Licensed under the MIT License. Default OU - Find OUs + Import from domain Lists the OUs of the domain this computer is joined to, using your Windows account. Select the ones to add. @@ -1976,7 +1976,7 @@ Licensed under the MIT License. Add selected OUs - Enter a display name and the distinguished name of the OU. The OU must belong to the domain above. + Enter a display name and the distinguished name of the OU. The OU must belong to the configured domain. Add an OU manually @@ -1990,6 +1990,12 @@ Licensed under the MIT License. Display name + + Add an OU manually or import OUs from the domain this computer is joined to. + + + OU actions + Password diff --git a/src/Foundry/Strings/es-ES/Resources.resw b/src/Foundry/Strings/es-ES/Resources.resw index 75a628cc9..2d041040c 100644 --- a/src/Foundry/Strings/es-ES/Resources.resw +++ b/src/Foundry/Strings/es-ES/Resources.resw @@ -1840,8 +1840,8 @@ Licenciado bajo la Licencia MIT. Búsqueda cancelada. - - Búsqueda completada. Seleccione las OU que quiere agregar. + + No se ha encontrado ninguna OU en el dominio de este equipo. Solo se han podido mostrar algunas OU. Puede agregar igualmente las que aparecen. @@ -1858,12 +1858,12 @@ Licenciado bajo la Licencia MIT. No se han agregado las OU. Pertenecen a un dominio distinto del nombre de dominio o de las OU ya incluidas en la lista. - - Se han agregado las OU seleccionadas. - Eliminar seleccionado + + OU seleccionadas: {0} de {1} + Introduzca la cuenta. @@ -1961,7 +1961,7 @@ Licenciado bajo la Licencia MIT. OU predeterminada - Buscar OU + Importar del dominio Muestra las OU del dominio al que está unido este equipo, con su cuenta de Windows. Seleccione las que quiere agregar. @@ -1976,7 +1976,7 @@ Licenciado bajo la Licencia MIT. Agregar las OU seleccionadas - Introduzca un nombre para mostrar y el nombre distintivo de la OU. La OU debe pertenecer al dominio indicado arriba. + Introduzca un nombre para mostrar y el nombre distintivo de la OU. La OU debe pertenecer al dominio configurado. Agregar una OU manualmente @@ -1990,6 +1990,12 @@ Licenciado bajo la Licencia MIT. Nombre para mostrar + + Agregue una OU manualmente o importe OU del dominio al que está unido este equipo. + + + Acciones de OU + Contraseña diff --git a/src/Foundry/Strings/es-MX/Resources.resw b/src/Foundry/Strings/es-MX/Resources.resw index c3abf6e7b..0d61b03c7 100644 --- a/src/Foundry/Strings/es-MX/Resources.resw +++ b/src/Foundry/Strings/es-MX/Resources.resw @@ -1840,8 +1840,8 @@ Licenciado bajo la Licencia MIT. Búsqueda cancelada. - - Búsqueda completada. Seleccione las OU que quiere agregar. + + No se ha encontrado ninguna OU en el dominio de este equipo. Solo se han podido mostrar algunas OU. Puede agregar igualmente las que aparecen. @@ -1858,12 +1858,12 @@ Licenciado bajo la Licencia MIT. No se han agregado las OU. Pertenecen a un dominio distinto del nombre de dominio o de las OU ya incluidas en la lista. - - Se han agregado las OU seleccionadas. - Eliminar seleccionado + + OU seleccionadas: {0} de {1} + Introduzca la cuenta. @@ -1961,7 +1961,7 @@ Licenciado bajo la Licencia MIT. OU predeterminada - Buscar OU + Importar del dominio Muestra las OU del dominio al que está unido este equipo, con su cuenta de Windows. Seleccione las que quiere agregar. @@ -1976,7 +1976,7 @@ Licenciado bajo la Licencia MIT. Agregar las OU seleccionadas - Introduzca un nombre para mostrar y el nombre distintivo de la OU. La OU debe pertenecer al dominio indicado arriba. + Introduzca un nombre para mostrar y el nombre distintivo de la OU. La OU debe pertenecer al dominio configurado. Agregar una OU manualmente @@ -1990,6 +1990,12 @@ Licenciado bajo la Licencia MIT. Nombre para mostrar + + Agregue una OU manualmente o importe OU del dominio al que está unido este equipo. + + + Acciones de OU + Contraseña diff --git a/src/Foundry/Strings/et-EE/Resources.resw b/src/Foundry/Strings/et-EE/Resources.resw index 5d4c7e6cf..d2dd08744 100644 --- a/src/Foundry/Strings/et-EE/Resources.resw +++ b/src/Foundry/Strings/et-EE/Resources.resw @@ -1840,8 +1840,8 @@ Litsentsitud MIT-i litsentsi alusel. Otsing tühistati. - - Otsing on lõpetatud. Valige lisatavad OU-d. + + Selle arvuti domeenist ei leitud ühtegi OU-d. Loetleda õnnestus ainult osa OU-sid. Kuvatud OU-sid saate siiski lisada. @@ -1858,12 +1858,12 @@ Litsentsitud MIT-i litsentsi alusel. OU-sid ei lisatud. Need kuuluvad teise domeeni kui domeeninimi või juba loendis olevad OU-d. - - Valitud OU-d lisati. - Eemalda valitud + + Valitud OU-d: {0}/{1} + Sisestage konto. @@ -1961,7 +1961,7 @@ Litsentsitud MIT-i litsentsi alusel. Vaikimisi OU - Otsi OU-sid + Impordi domeenist Loetleb teie Windowsi konto abil selle domeeni OU-d, millega see arvuti on liidetud. Valige lisatavad OU-d. @@ -1976,7 +1976,7 @@ Litsentsitud MIT-i litsentsi alusel. Lisa valitud OU-d - Sisestage OU kuvatav nimi ja eristusnimi. OU peab kuuluma ülaltoodud domeeni. + Sisestage OU kuvatav nimi ja eristusnimi. OU peab kuuluma konfigureeritud domeeni. Lisa OU käsitsi @@ -1990,6 +1990,12 @@ Litsentsitud MIT-i litsentsi alusel. Kuvatav nimi + + Lisage OU käsitsi või importige OU-d domeenist, millega see arvuti on liidetud. + + + OU toimingud + Parool diff --git a/src/Foundry/Strings/fi-FI/Resources.resw b/src/Foundry/Strings/fi-FI/Resources.resw index 6ec614d9f..d44ab6c4f 100644 --- a/src/Foundry/Strings/fi-FI/Resources.resw +++ b/src/Foundry/Strings/fi-FI/Resources.resw @@ -1840,8 +1840,8 @@ Lisensoitu MIT-lisenssillä. Haku peruutettiin. - - Haku valmistui. Valitse lisättävät OU:t. + + Tämän tietokoneen toimialueesta ei löytynyt OU:ita. Vain osa OU:ista voitiin luetella. Voit silti lisätä näkyvissä olevat. @@ -1858,12 +1858,12 @@ Lisensoitu MIT-lisenssillä. OU:ita ei lisätty. Ne kuuluvat eri toimialueeseen kuin toimialueen nimi tai luettelossa jo olevat OU:t. - - Valitut OU:t lisättiin. - Poista valitut + + Valitut OU:t: {0} / {1} + Anna tili. @@ -1961,7 +1961,7 @@ Lisensoitu MIT-lisenssillä. Oletus-OU - Etsi OU:ita + Tuo toimialueesta Luettelee Windows-tilisi avulla sen toimialueen OU:t, johon tämä tietokone on liitetty. Valitse lisättävät. @@ -1976,7 +1976,7 @@ Lisensoitu MIT-lisenssillä. Lisää valitut OU:t - Anna näyttönimi ja OU:n yksilöivä nimi. OU:n on kuuluttava yllä olevaan toimialueeseen. + Anna näyttönimi ja OU:n yksilöivä nimi. OU:n on kuuluttava määritettyyn toimialueeseen. Lisää OU manuaalisesti @@ -1990,6 +1990,12 @@ Lisensoitu MIT-lisenssillä. Näyttönimi + + Lisää OU manuaalisesti tai tuo OU:t toimialueesta, johon tämä tietokone on liitetty. + + + OU-toiminnot + Salasana diff --git a/src/Foundry/Strings/fr-CA/Resources.resw b/src/Foundry/Strings/fr-CA/Resources.resw index d17acf933..127a24e3c 100644 --- a/src/Foundry/Strings/fr-CA/Resources.resw +++ b/src/Foundry/Strings/fr-CA/Resources.resw @@ -1840,8 +1840,8 @@ Sous licence MIT. Recherche annulée. - - Recherche terminée. Sélectionnez les OU à ajouter. + + Aucune OU n’a été trouvée dans le domaine de cet ordinateur. Seule une partie des OU a pu être répertoriée. Vous pouvez tout de même ajouter celles qui sont affichées. @@ -1858,12 +1858,12 @@ Sous licence MIT. Les OU n’ont pas été ajoutées. Elles appartiennent à un domaine différent du nom de domaine ou des OU déjà répertoriées. - - Les OU sélectionnées ont été ajoutées. - Supprimer la sélection + + OU sélectionnées: {0} de {1} + Saisissez le compte. @@ -1961,7 +1961,7 @@ Sous licence MIT. OU par défaut - Rechercher des OU + Importer depuis le domaine Répertorie les OU du domaine auquel cet ordinateur est joint, à l’aide de votre compte Windows. Sélectionnez celles à ajouter. @@ -1976,7 +1976,7 @@ Sous licence MIT. Ajouter les OU sélectionnées - Saisissez un nom d’affichage et le nom distinctif de l’OU. L’OU doit appartenir au domaine ci-dessus. + Saisissez un nom d’affichage et le nom distinctif de l’OU. L’OU doit appartenir au domaine configuré. Ajouter une OU manuellement @@ -1990,6 +1990,12 @@ Sous licence MIT. Nom d’affichage + + Ajoutez une OU manuellement ou importez des OU depuis le domaine auquel cet ordinateur est joint. + + + Actions sur les OU + Mot de passe diff --git a/src/Foundry/Strings/fr-FR/Resources.resw b/src/Foundry/Strings/fr-FR/Resources.resw index f47a6ba37..c39c0aeea 100644 --- a/src/Foundry/Strings/fr-FR/Resources.resw +++ b/src/Foundry/Strings/fr-FR/Resources.resw @@ -1840,8 +1840,8 @@ Sous licence MIT. Recherche annulée. - - Recherche terminée. Sélectionnez les OU à ajouter. + + Aucune OU n’a été trouvée dans le domaine de cet ordinateur. Seule une partie des OU a pu être répertoriée. Vous pouvez tout de même ajouter celles qui sont affichées. @@ -1858,12 +1858,12 @@ Sous licence MIT. Les OU n’ont pas été ajoutées. Elles appartiennent à un domaine différent du nom de domaine ou des OU déjà répertoriées. - - Les OU sélectionnées ont été ajoutées. - Supprimer la sélection + + OU sélectionnées : {0} sur {1} + Saisissez le compte. @@ -1961,7 +1961,7 @@ Sous licence MIT. OU par défaut - Rechercher des OU + Importer depuis le domaine Répertorie les OU du domaine auquel cet ordinateur est joint, à l’aide de votre compte Windows. Sélectionnez celles à ajouter. @@ -1976,7 +1976,7 @@ Sous licence MIT. Ajouter les OU sélectionnées - Saisissez un nom d’affichage et le nom distinctif de l’OU. L’OU doit appartenir au domaine ci-dessus. + Saisissez un nom d’affichage et le nom distinctif de l’OU. L’OU doit appartenir au domaine configuré. Ajouter une OU manuellement @@ -1990,6 +1990,12 @@ Sous licence MIT. Nom d’affichage + + Ajoutez une OU manuellement ou importez des OU depuis le domaine auquel cet ordinateur est joint. + + + Actions sur les OU + Mot de passe diff --git a/src/Foundry/Strings/he-IL/Resources.resw b/src/Foundry/Strings/he-IL/Resources.resw index 28a29ffaa..5779b74b7 100644 --- a/src/Foundry/Strings/he-IL/Resources.resw +++ b/src/Foundry/Strings/he-IL/Resources.resw @@ -1840,8 +1840,8 @@ החיפוש בוטל. - - החיפוש הושלם. בחר את יחידות ה-OU להוספה. + + לא נמצאו יחידות OU בתחום של מחשב זה. ניתן היה להציג רק חלק מיחידות ה-OU. עדיין ניתן להוסיף את אלה שמוצגות. @@ -1858,12 +1858,12 @@ יחידות ה-OU לא נוספו. הן שייכות לתחום שונה משם התחום או מיחידות ה-OU שכבר ברשימה. - - יחידות ה-OU שנבחרו נוספו. - הסר את הבחירה + + יחידות OU שנבחרו: {0} מתוך {1} + הזן את החשבון. @@ -1961,7 +1961,7 @@ OU ברירת מחדל - חיפוש יחידות OU + ייבוא מהתחום מציג את יחידות ה-OU של התחום שאליו מחשב זה מצורף, באמצעות חשבון ה-Windows שלך. בחר את אלה שברצונך להוסיף. @@ -1976,7 +1976,7 @@ הוספת יחידות ה-OU שנבחרו - הזן שם תצוגה ואת השם המובחן של ה-OU. ה-OU חייבת להשתייך לתחום שלמעלה. + הזן שם תצוגה ואת השם המובחן של ה-OU. ה-OU חייבת להשתייך לתחום שהוגדר. הוספת OU ידנית @@ -1990,6 +1990,12 @@ שם תצוגה + + הוסף OU ידנית או ייבא יחידות OU מהתחום שאליו מחשב זה מצורף. + + + פעולות OU + סיסמה diff --git a/src/Foundry/Strings/hr-HR/Resources.resw b/src/Foundry/Strings/hr-HR/Resources.resw index cd57ea36b..3d005028a 100644 --- a/src/Foundry/Strings/hr-HR/Resources.resw +++ b/src/Foundry/Strings/hr-HR/Resources.resw @@ -1840,8 +1840,8 @@ Licenciran prema MIT licenci. Pretraživanje je otkazano. - - Pretraživanje je dovršeno. Odaberite OU-ove koje želite dodati. + + U domeni ovog računala nije pronađen nijedan OU. Mogao se prikazati samo dio OU-ova. Prikazane i dalje možete dodati. @@ -1858,12 +1858,12 @@ Licenciran prema MIT licenci. OU-ovi nisu dodani. Pripadaju domeni koja se razlikuje od naziva domene ili od već navedenih OU-ova. - - Odabrani OU-ovi su dodani. - Ukloni odabrano + + Odabrani OU-ovi: {0} od {1} + Unesite račun. @@ -1961,7 +1961,7 @@ Licenciran prema MIT licenci. Zadani OU - Pronađi OU-ove + Uvoz iz domene Navodi OU-ove domene kojoj je ovo računalo pridruženo, koristeći vaš Windows račun. Odaberite one koje želite dodati. @@ -1976,7 +1976,7 @@ Licenciran prema MIT licenci. Dodaj odabrane OU-ove - Unesite naziv za prikaz i razlikovni naziv OU-a. OU mora pripadati gore navedenoj domeni. + Unesite naziv za prikaz i razlikovni naziv OU-a. OU mora pripadati konfiguriranoj domeni. Ručno dodaj OU @@ -1990,6 +1990,12 @@ Licenciran prema MIT licenci. Naziv za prikaz + + Dodajte OU ručno ili uvezite OU-ove iz domene kojoj je ovo računalo pridruženo. + + + Radnje za OU-ove + Lozinka diff --git a/src/Foundry/Strings/hu-HU/Resources.resw b/src/Foundry/Strings/hu-HU/Resources.resw index 2d67a0c52..bd61bc3df 100644 --- a/src/Foundry/Strings/hu-HU/Resources.resw +++ b/src/Foundry/Strings/hu-HU/Resources.resw @@ -1840,8 +1840,8 @@ Licenc az MIT licenc alatt. A keresés megszakítva. - - A keresés befejeződött. Jelölje ki a hozzáadni kívánt OU-kat. + + Nem található OU a számítógép tartományában. Csak az OU-k egy részét sikerült listázni. A megjelenítetteket így is hozzáadhatja. @@ -1858,12 +1858,12 @@ Licenc az MIT licenc alatt. Az OU-k nem lettek hozzáadva. Más tartományhoz tartoznak, mint a tartománynév vagy a már felsorolt OU-k. - - A kijelölt OU-k hozzáadása megtörtént. - A kiválasztott eltávolítása + + Kijelölt OU-k: {0}/{1} + Adja meg a fiókot. @@ -1961,7 +1961,7 @@ Licenc az MIT licenc alatt. Alapértelmezett OU - OU-k keresése + Importálás a tartományból Az Ön Windows-fiókjával listázza annak a tartománynak az OU-it, amelyhez ez a számítógép csatlakozik. Jelölje ki a hozzáadni kívántakat. @@ -1976,7 +1976,7 @@ Licenc az MIT licenc alatt. Kijelölt OU-k hozzáadása - Adja meg az OU megjelenítendő nevét és megkülönböztető nevét. Az OU-nak a fenti tartományhoz kell tartoznia. + Adja meg az OU megjelenítendő nevét és megkülönböztető nevét. Az OU-nak a beállított tartományhoz kell tartoznia. OU kézi hozzáadása @@ -1990,6 +1990,12 @@ Licenc az MIT licenc alatt. Megjelenítendő név + + Adjon hozzá egy OU-t kézzel, vagy importáljon OU-kat abból a tartományból, amelyhez ez a számítógép csatlakozik. + + + OU-műveletek + Jelszó diff --git a/src/Foundry/Strings/it-IT/Resources.resw b/src/Foundry/Strings/it-IT/Resources.resw index a1ca4d07b..8dc9990f5 100644 --- a/src/Foundry/Strings/it-IT/Resources.resw +++ b/src/Foundry/Strings/it-IT/Resources.resw @@ -1840,8 +1840,8 @@ Concesso in licenza con la licenza MIT. Ricerca annullata. - - Ricerca completata. Seleziona le OU da aggiungere. + + Nessuna OU trovata nel dominio di questo computer. È stato possibile elencare solo alcune OU. Puoi comunque aggiungere quelle mostrate. @@ -1858,12 +1858,12 @@ Concesso in licenza con la licenza MIT. Le OU non sono state aggiunte. Appartengono a un dominio diverso dal nome di dominio o dalle OU già elencate. - - Le OU selezionate sono state aggiunte. - Rimuovi selezionato + + OU selezionate: {0} di {1} + Inserisci l'account. @@ -1961,7 +1961,7 @@ Concesso in licenza con la licenza MIT. OU predefinita - Cerca OU + Importa dal dominio Elenca le OU del dominio a cui è aggiunto questo computer, usando il tuo account Windows. Seleziona quelle da aggiungere. @@ -1976,7 +1976,7 @@ Concesso in licenza con la licenza MIT. Aggiungi OU selezionate - Inserisci un nome visualizzato e il nome distinto della OU. La OU deve appartenere al dominio indicato sopra. + Inserisci un nome visualizzato e il nome distinto della OU. La OU deve appartenere al dominio configurato. Aggiungi una OU manualmente @@ -1990,6 +1990,12 @@ Concesso in licenza con la licenza MIT. Nome visualizzato + + Aggiungi una OU manualmente o importa le OU dal dominio a cui è aggiunto questo computer. + + + Azioni OU + Password diff --git a/src/Foundry/Strings/ja-JP/Resources.resw b/src/Foundry/Strings/ja-JP/Resources.resw index 002220c2f..457065757 100644 --- a/src/Foundry/Strings/ja-JP/Resources.resw +++ b/src/Foundry/Strings/ja-JP/Resources.resw @@ -1840,8 +1840,8 @@ MITライセンスに基づいてライセンスされています。 検索をキャンセルしました。 - - 検索が完了しました。追加するOUを選択してください。 + + このコンピューターのドメインにOUが見つかりませんでした。 一部のOUのみ一覧表示できました。表示されているOUは追加できます。 @@ -1858,12 +1858,12 @@ MITライセンスに基づいてライセンスされています。 OUは追加されませんでした。ドメイン名または既に一覧にあるOUとは異なるドメインに属しています。 - - 選択したOUを追加しました。 - 選択したものを削除 + + 選択されたOU: {0} / {1} + アカウントを入力してください。 @@ -1961,7 +1961,7 @@ MITライセンスに基づいてライセンスされています。 既定のOU - OUを検索 + ドメインからインポート Windowsアカウントを使用して、このコンピューターが参加しているドメインのOUを一覧表示します。追加するOUを選択してください。 @@ -1976,7 +1976,7 @@ MITライセンスに基づいてライセンスされています。 選択したOUを追加 - OUの表示名と識別名を入力してください。OUは上記のドメインに属している必要があります。 + OUの表示名と識別名を入力してください。OUは構成済みのドメインに属している必要があります。 OUを手動で追加 @@ -1990,6 +1990,12 @@ MITライセンスに基づいてライセンスされています。 表示名 + + OUを手動で追加するか、このコンピューターが参加しているドメインからOUをインポートします。 + + + OUアクション + パスワード diff --git a/src/Foundry/Strings/ko-KR/Resources.resw b/src/Foundry/Strings/ko-KR/Resources.resw index 995bb922b..c7dfd2285 100644 --- a/src/Foundry/Strings/ko-KR/Resources.resw +++ b/src/Foundry/Strings/ko-KR/Resources.resw @@ -1840,8 +1840,8 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 검색이 취소되었습니다. - - 검색이 완료되었습니다. 추가할 OU를 선택하세요. + + 이 컴퓨터의 도메인에서 OU를 찾을 수 없습니다. 일부 OU만 표시할 수 있었습니다. 표시된 OU는 추가할 수 있습니다. @@ -1858,12 +1858,12 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. OU가 추가되지 않았습니다. 도메인 이름 또는 이미 목록에 있는 OU와 다른 도메인에 속해 있습니다. - - 선택한 OU를 추가했습니다. - 선택 항목 제거 + + 선택된 OU: {0}/{1} + 계정을 입력하세요. @@ -1961,7 +1961,7 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 기본 OU - OU 찾기 + 도메인에서 가져오기 Windows 계정을 사용하여 이 컴퓨터가 가입된 도메인의 OU를 표시합니다. 추가할 OU를 선택하세요. @@ -1976,7 +1976,7 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 선택한 OU 추가 - OU의 표시 이름과 고유 이름을 입력하세요. OU는 위의 도메인에 속해야 합니다. + OU의 표시 이름과 고유 이름을 입력하세요. OU는 구성된 도메인에 속해야 합니다. OU 수동 추가 @@ -1990,6 +1990,12 @@ MIT 라이선스에 따라 라이선스가 부여됩니다. 표시 이름 + + OU를 수동으로 추가하거나 이 컴퓨터가 가입된 도메인에서 OU를 가져옵니다. + + + OU 작업 + 암호 diff --git a/src/Foundry/Strings/lt-LT/Resources.resw b/src/Foundry/Strings/lt-LT/Resources.resw index 2913c6549..e31b03553 100644 --- a/src/Foundry/Strings/lt-LT/Resources.resw +++ b/src/Foundry/Strings/lt-LT/Resources.resw @@ -1840,8 +1840,8 @@ Licencijuota pagal MIT licenciją. Paieška atšaukta. - - Paieška baigta. Pasirinkite OU, kuriuos norite pridėti. + + Šio kompiuterio domene OU nerasta. Pavyko pateikti tik dalį OU. Rodomus vis tiek galite pridėti. @@ -1858,12 +1858,12 @@ Licencijuota pagal MIT licenciją. OU nepridėti. Jie priklauso kitam domenui nei nurodytas domeno vardas arba jau sąraše esantys OU. - - Pasirinkti OU pridėti. - Pašalinti pasirinktą + + Pasirinkti OU: {0} iš {1} + Įveskite paskyrą. @@ -1961,7 +1961,7 @@ Licencijuota pagal MIT licenciją. Numatytasis OU - Rasti OU + Importuoti iš domeno Naudojant jūsų Windows paskyrą pateikiamas domeno, prie kurio prijungtas šis kompiuteris, OU sąrašas. Pasirinkite, kuriuos pridėti. @@ -1976,7 +1976,7 @@ Licencijuota pagal MIT licenciją. Pridėti pasirinktus OU - Įveskite OU rodomą pavadinimą ir skiriamąjį vardą. OU turi priklausyti aukščiau nurodytam domenui. + Įveskite OU rodomą pavadinimą ir skiriamąjį vardą. OU turi priklausyti sukonfigūruotam domenui. Pridėti OU rankiniu būdu @@ -1990,6 +1990,12 @@ Licencijuota pagal MIT licenciją. Rodomas pavadinimas + + Pridėkite OU rankiniu būdu arba importuokite OU iš domeno, prie kurio prijungtas šis kompiuteris. + + + OU veiksmai + Slaptažodis diff --git a/src/Foundry/Strings/lv-LV/Resources.resw b/src/Foundry/Strings/lv-LV/Resources.resw index f3573ba15..ba94afe19 100644 --- a/src/Foundry/Strings/lv-LV/Resources.resw +++ b/src/Foundry/Strings/lv-LV/Resources.resw @@ -1840,8 +1840,8 @@ Licencēts saskaņā ar MIT licenci. Meklēšana atcelta. - - Meklēšana pabeigta. Atlasiet pievienojamās OU. + + Šī datora domēnā netika atrasta neviena OU. Izdevās parādīt tikai daļu OU. Parādītās joprojām varat pievienot. @@ -1858,12 +1858,12 @@ Licencēts saskaņā ar MIT licenci. OU netika pievienotas. Tās pieder citam domēnam, nevis norādītajam domēna nosaukumam vai sarakstā jau esošajām OU. - - Atlasītās OU tika pievienotas. - Noņemt atlasīto + + Atlasītās OU: {0} no {1} + Ievadiet kontu. @@ -1961,7 +1961,7 @@ Licencēts saskaņā ar MIT licenci. Noklusējuma OU - Atrast OU + Importēt no domēna Parāda tā domēna OU, kuram pievienots šis dators, izmantojot jūsu Windows kontu. Atlasiet pievienojamās. @@ -1976,7 +1976,7 @@ Licencēts saskaņā ar MIT licenci. Pievienot atlasītās OU - Ievadiet OU parādāmo nosaukumu un atšķirīgo nosaukumu. OU jāpieder iepriekš norādītajam domēnam. + Ievadiet OU parādāmo nosaukumu un atšķirīgo nosaukumu. OU jāpieder konfigurētajam domēnam. Pievienot OU manuāli @@ -1990,6 +1990,12 @@ Licencēts saskaņā ar MIT licenci. Parādāmais nosaukums + + Pievienojiet OU manuāli vai importējiet OU no domēna, kuram pievienots šis dators. + + + OU darbības + Parole diff --git a/src/Foundry/Strings/nb-NO/Resources.resw b/src/Foundry/Strings/nb-NO/Resources.resw index ef227b817..c6a514777 100644 --- a/src/Foundry/Strings/nb-NO/Resources.resw +++ b/src/Foundry/Strings/nb-NO/Resources.resw @@ -1840,8 +1840,8 @@ Lisensiert under MIT-lisensen. Søket ble avbrutt. - - Søket er fullført. Velg OU-ene du vil legge til. + + Fant ingen OU-er i denne datamaskinens domene. Bare noen av OU-ene kunne vises. Du kan fortsatt legge til de som vises. @@ -1858,12 +1858,12 @@ Lisensiert under MIT-lisensen. OU-ene ble ikke lagt til. De tilhører et annet domene enn domenenavnet eller OU-ene som allerede står i listen. - - De valgte OU-ene ble lagt til. - Fjern valgte + + Valgte OU-er: {0} av {1} + Angi kontoen. @@ -1961,7 +1961,7 @@ Lisensiert under MIT-lisensen. Standard-OU - Finn OU-er + Importer fra domene Viser OU-ene i domenet denne datamaskinen er koblet til, ved hjelp av Windows-kontoen din. Velg de du vil legge til. @@ -1976,7 +1976,7 @@ Lisensiert under MIT-lisensen. Legg til valgte OU-er - Angi et visningsnavn og det unike navnet til OU-en. OU-en må tilhøre domenet ovenfor. + Angi et visningsnavn og det unike navnet til OU-en. OU-en må tilhøre det konfigurerte domenet. Legg til en OU manuelt @@ -1990,6 +1990,12 @@ Lisensiert under MIT-lisensen. Visningsnavn + + Legg til en OU manuelt, eller importer OU-er fra domenet denne datamaskinen er koblet til. + + + OU-handlinger + Passord diff --git a/src/Foundry/Strings/nl-NL/Resources.resw b/src/Foundry/Strings/nl-NL/Resources.resw index 048d6575d..d2b08f6ad 100644 --- a/src/Foundry/Strings/nl-NL/Resources.resw +++ b/src/Foundry/Strings/nl-NL/Resources.resw @@ -1840,8 +1840,8 @@ Gelicentieerd onder de MIT-licentie. Zoeken geannuleerd. - - Zoeken voltooid. Selecteer de OU's die u wilt toevoegen. + + Er zijn geen OU's gevonden in het domein van deze computer. Slechts een deel van de OU's kon worden weergegeven. U kunt de weergegeven OU's nog steeds toevoegen. @@ -1858,12 +1858,12 @@ Gelicentieerd onder de MIT-licentie. De OU's zijn niet toegevoegd. Ze behoren tot een ander domein dan de domeinnaam of de OU's die al in de lijst staan. - - De geselecteerde OU's zijn toegevoegd. - Geselecteerde verwijderen + + Geselecteerde OU's: {0} van {1} + Voer het account in. @@ -1961,7 +1961,7 @@ Gelicentieerd onder de MIT-licentie. Standaard-OU - OU's zoeken + Importeren uit domein Geeft met uw Windows-account de OU's weer van het domein waarvan deze computer lid is. Selecteer de OU's die u wilt toevoegen. @@ -1976,7 +1976,7 @@ Gelicentieerd onder de MIT-licentie. Geselecteerde OU's toevoegen - Voer een weergavenaam en de DN-naam van de OU in. De OU moet tot het bovenstaande domein behoren. + Voer een weergavenaam en de DN-naam van de OU in. De OU moet tot het geconfigureerde domein behoren. Handmatig een OU toevoegen @@ -1990,6 +1990,12 @@ Gelicentieerd onder de MIT-licentie. Weergavenaam + + Voeg handmatig een OU toe of importeer OU's uit het domein waarvan deze computer lid is. + + + OU-acties + Wachtwoord diff --git a/src/Foundry/Strings/pl-PL/Resources.resw b/src/Foundry/Strings/pl-PL/Resources.resw index a4f2cad32..a98fe6a5c 100644 --- a/src/Foundry/Strings/pl-PL/Resources.resw +++ b/src/Foundry/Strings/pl-PL/Resources.resw @@ -1840,8 +1840,8 @@ Licencjonowany na licencji MIT. Wyszukiwanie anulowano. - - Wyszukiwanie zakończono. Wybierz jednostki OU do dodania. + + Nie znaleziono żadnych jednostek OU w domenie tego komputera. Udało się wyświetlić tylko część jednostek OU. Wyświetlone jednostki nadal można dodać. @@ -1858,12 +1858,12 @@ Licencjonowany na licencji MIT. Nie dodano jednostek OU. Należą one do innej domeny niż podana nazwa domeny lub jednostki OU już znajdujące się na liście. - - Dodano wybrane jednostki OU. - Usuń wybrane + + Wybrane jednostki OU: {0} z {1} + Wprowadź konto. @@ -1961,7 +1961,7 @@ Licencjonowany na licencji MIT. Domyślna jednostka OU - Znajdź jednostki OU + Importuj z domeny Wyświetla jednostki OU domeny, do której dołączony jest ten komputer, przy użyciu Twojego konta Windows. Wybierz te, które chcesz dodać. @@ -1976,7 +1976,7 @@ Licencjonowany na licencji MIT. Dodaj wybrane jednostki OU - Wprowadź nazwę wyświetlaną i nazwę wyróżniającą jednostki OU. Jednostka OU musi należeć do domeny podanej powyżej. + Wprowadź nazwę wyświetlaną i nazwę wyróżniającą jednostki OU. Jednostka OU musi należeć do skonfigurowanej domeny. Dodaj jednostkę OU ręcznie @@ -1990,6 +1990,12 @@ Licencjonowany na licencji MIT. Nazwa wyświetlana + + Dodaj jednostkę OU ręcznie lub zaimportuj jednostki OU z domeny, do której dołączony jest ten komputer. + + + Działania jednostek OU + Hasło diff --git a/src/Foundry/Strings/pt-BR/Resources.resw b/src/Foundry/Strings/pt-BR/Resources.resw index 5770f1ddb..c7d7d2100 100644 --- a/src/Foundry/Strings/pt-BR/Resources.resw +++ b/src/Foundry/Strings/pt-BR/Resources.resw @@ -1840,8 +1840,8 @@ Licenciado sob a licença MIT. Busca cancelada. - - Busca concluída. Selecione as OUs a adicionar. + + Nenhuma OU foi encontrada no domínio deste computador. Só foi possível listar algumas das OUs. Você ainda pode adicionar as que aparecem. @@ -1858,12 +1858,12 @@ Licenciado sob a licença MIT. As OUs não foram adicionadas. Elas pertencem a um domínio diferente do nome do domínio ou das OUs já listadas. - - As OUs selecionadas foram adicionadas. - Remover selecionado + + OUs selecionadas: {0} de {1} + Insira a conta. @@ -1961,7 +1961,7 @@ Licenciado sob a licença MIT. OU padrão - Localizar OUs + Importar do domínio Lista as OUs do domínio ao qual este computador está associado, usando sua conta do Windows. Selecione as que deseja adicionar. @@ -1976,7 +1976,7 @@ Licenciado sob a licença MIT. Adicionar OUs selecionadas - Insira um nome de exibição e o nome distinto da OU. A OU deve pertencer ao domínio acima. + Insira um nome de exibição e o nome distinto da OU. A OU deve pertencer ao domínio configurado. Adicionar uma OU manualmente @@ -1990,6 +1990,12 @@ Licenciado sob a licença MIT. Nome de exibição + + Adicione uma OU manualmente ou importe OUs do domínio ao qual este computador está associado. + + + Ações de OU + Senha diff --git a/src/Foundry/Strings/pt-PT/Resources.resw b/src/Foundry/Strings/pt-PT/Resources.resw index 437261ce1..00c70e552 100644 --- a/src/Foundry/Strings/pt-PT/Resources.resw +++ b/src/Foundry/Strings/pt-PT/Resources.resw @@ -1840,8 +1840,8 @@ Licenciado sob a licença MIT. Procura cancelada. - - Procura concluída. Selecione as OU a adicionar. + + Não foi encontrada nenhuma OU no domínio deste computador. Só foi possível listar algumas das OU. Pode adicionar na mesma as que são apresentadas. @@ -1858,12 +1858,12 @@ Licenciado sob a licença MIT. As OU não foram adicionadas. Pertencem a um domínio diferente do nome do domínio ou das OU já listadas. - - As OU selecionadas foram adicionadas. - Remover selecionado + + OU selecionadas: {0} de {1} + Introduza a conta. @@ -1961,7 +1961,7 @@ Licenciado sob a licença MIT. OU predefinida - Procurar OU + Importar do domínio Lista as OU do domínio a que este computador está associado, utilizando a sua conta Windows. Selecione as que pretende adicionar. @@ -1976,7 +1976,7 @@ Licenciado sob a licença MIT. Adicionar OU selecionadas - Introduza um nome a apresentar e o nome distinto da OU. A OU tem de pertencer ao domínio indicado acima. + Introduza um nome a apresentar e o nome distinto da OU. A OU tem de pertencer ao domínio configurado. Adicionar uma OU manualmente @@ -1990,6 +1990,12 @@ Licenciado sob a licença MIT. Nome a apresentar + + Adicione uma OU manualmente ou importe OU do domínio a que este computador está associado. + + + Ações de OU + Palavra-passe diff --git a/src/Foundry/Strings/ro-RO/Resources.resw b/src/Foundry/Strings/ro-RO/Resources.resw index 2dd8f42b9..cc84a5f08 100644 --- a/src/Foundry/Strings/ro-RO/Resources.resw +++ b/src/Foundry/Strings/ro-RO/Resources.resw @@ -1840,8 +1840,8 @@ Licențiat sub licența MIT. Căutare anulată. - - Căutare încheiată. Selectați OU-urile de adăugat. + + Nu s-a găsit niciun OU în domeniul acestui computer. Doar o parte dintre OU-uri au putut fi listate. Le puteți adăuga în continuare pe cele afișate. @@ -1858,12 +1858,12 @@ Licențiat sub licența MIT. OU-urile nu au fost adăugate. Aparțin altui domeniu decât numele domeniului sau OU-urile deja listate. - - OU-urile selectate au fost adăugate. - Eliminați selectat + + OU-uri selectate: {0} din {1} + Introduceți contul. @@ -1961,7 +1961,7 @@ Licențiat sub licența MIT. OU implicit - Căutare OU-uri + Import din domeniu Listează OU-urile domeniului la care este asociat acest computer, folosind contul dvs. Windows. Selectați-le pe cele de adăugat. @@ -1976,7 +1976,7 @@ Licențiat sub licența MIT. Adăugare OU-uri selectate - Introduceți un nume afișat și numele distinctiv al OU-ului. OU-ul trebuie să aparțină domeniului de mai sus. + Introduceți un nume afișat și numele distinctiv al OU-ului. OU-ul trebuie să aparțină domeniului configurat. Adăugarea manuală a unui OU @@ -1990,6 +1990,12 @@ Licențiat sub licența MIT. Nume afișat + + Adăugați manual un OU sau importați OU-uri din domeniul la care este asociat acest computer. + + + Acțiuni pentru OU-uri + Parolă diff --git a/src/Foundry/Strings/ru-RU/Resources.resw b/src/Foundry/Strings/ru-RU/Resources.resw index a36838c25..d440cb71c 100644 --- a/src/Foundry/Strings/ru-RU/Resources.resw +++ b/src/Foundry/Strings/ru-RU/Resources.resw @@ -1840,8 +1840,8 @@ Поиск отменен. - - Поиск завершен. Выберите OU для добавления. + + В домене этого компьютера не найдено ни одного OU. Удалось получить только часть OU. Показанные OU все равно можно добавить. @@ -1858,12 +1858,12 @@ OU не добавлены. Они принадлежат другому домену, отличному от указанного имени домена или от уже добавленных в список OU. - - Выбранные OU добавлены. - Удалить выбранное + + Выбранные OU: {0} из {1} + Укажите учетную запись. @@ -1961,7 +1961,7 @@ OU по умолчанию - Найти OU + Импорт из домена Выводит список OU домена, к которому присоединен этот компьютер, используя вашу учетную запись Windows. Выберите OU для добавления. @@ -1976,7 +1976,7 @@ Добавить выбранные OU - Укажите отображаемое имя и различающееся имя OU. OU должно принадлежать домену, указанному выше. + Укажите отображаемое имя и различающееся имя OU. OU должно принадлежать настроенному домену. Добавить OU вручную @@ -1990,6 +1990,12 @@ Отображаемое имя + + Добавьте OU вручную или импортируйте OU из домена, к которому присоединен этот компьютер. + + + Действия с OU + Пароль diff --git a/src/Foundry/Strings/sk-SK/Resources.resw b/src/Foundry/Strings/sk-SK/Resources.resw index cb0c0c7c1..2e691c680 100644 --- a/src/Foundry/Strings/sk-SK/Resources.resw +++ b/src/Foundry/Strings/sk-SK/Resources.resw @@ -1840,8 +1840,8 @@ Licencované podľa licencie MIT. Vyhľadávanie bolo zrušené. - - Vyhľadávanie sa dokončilo. Vyberte OU, ktoré chcete pridať. + + V doméne tohto počítača sa nenašli žiadne OU. Podarilo sa načítať len niektoré OU. Zobrazené OU môžete napriek tomu pridať. @@ -1858,12 +1858,12 @@ Licencované podľa licencie MIT. OU neboli pridané. Patria do inej domény, než je zadaný názov domény alebo doména už uvedených OU. - - Vybrané OU boli pridané. - Odstrániť vybraté + + Vybrané OU: {0} z {1} + Zadajte účet. @@ -1961,7 +1961,7 @@ Licencované podľa licencie MIT. Predvolená OU - Vyhľadať OU + Importovať z domény Zobrazí OU domény, ku ktorej je tento počítač pripojený, pomocou vášho účtu Windows. Vyberte tie, ktoré chcete pridať. @@ -1976,7 +1976,7 @@ Licencované podľa licencie MIT. Pridať vybrané OU - Zadajte zobrazovaný názov a rozlišujúci názov OU. OU musí patriť do domény uvedenej vyššie. + Zadajte zobrazovaný názov a rozlišujúci názov OU. OU musí patriť do nakonfigurovanej domény. Pridať OU ručne @@ -1990,6 +1990,12 @@ Licencované podľa licencie MIT. Zobrazovaný názov + + Pridajte OU ručne alebo importujte OU z domény, ku ktorej je tento počítač pripojený. + + + Akcie OU + Heslo diff --git a/src/Foundry/Strings/sl-SI/Resources.resw b/src/Foundry/Strings/sl-SI/Resources.resw index af3eaeff9..54a84c184 100644 --- a/src/Foundry/Strings/sl-SI/Resources.resw +++ b/src/Foundry/Strings/sl-SI/Resources.resw @@ -1840,8 +1840,8 @@ Licencirano pod licenco MIT. Iskanje je preklicano. - - Iskanje je končano. Izberite OU, ki jih želite dodati. + + V domeni tega računalnika ni bilo najdenih OU. Prikazati je bilo mogoče le nekatere OU. Prikazane lahko vseeno dodate. @@ -1858,12 +1858,12 @@ Licencirano pod licenco MIT. OU niso bile dodane. Pripadajo drugi domeni kot ime domene ali že navedene OU. - - Izbrane OU so bile dodane. - Odstrani izbrano + + Izbrane OU: {0} od {1} + Vnesite račun. @@ -1961,7 +1961,7 @@ Licencirano pod licenco MIT. Privzeta OU - Poišči OU + Uvozi iz domene Z vašim računom Windows prikaže OU domene, ki ji je pridružen ta računalnik. Izberite tiste, ki jih želite dodati. @@ -1976,7 +1976,7 @@ Licencirano pod licenco MIT. Dodaj izbrane OU - Vnesite prikazno ime in razločevalno ime OU. OU mora pripadati zgoraj navedeni domeni. + Vnesite prikazno ime in razločevalno ime OU. OU mora pripadati konfigurirani domeni. Ročno dodajanje OU @@ -1990,6 +1990,12 @@ Licencirano pod licenco MIT. Prikazno ime + + Ročno dodajte OU ali uvozite OU iz domene, ki ji je pridružen ta računalnik. + + + Dejanja OU + Geslo diff --git a/src/Foundry/Strings/sr-Latn-RS/Resources.resw b/src/Foundry/Strings/sr-Latn-RS/Resources.resw index faccc97a5..ac524427c 100644 --- a/src/Foundry/Strings/sr-Latn-RS/Resources.resw +++ b/src/Foundry/Strings/sr-Latn-RS/Resources.resw @@ -1840,8 +1840,8 @@ Licencirano pod MIT licencom. Pretraga je otkazana. - - Pretraga je završena. Izaberite OU koje želite da dodate. + + Nijedna OU nije pronađena u domenu ovog računara. Samo neke OU su mogle da budu prikazane. Prikazane i dalje možete da dodate. @@ -1858,12 +1858,12 @@ Licencirano pod MIT licencom. OU nisu dodate. Pripadaju drugom domenu, a ne unetom imenu domena ili domenu već navedenih OU. - - Izabrane OU su dodate. - Ukloni izabrano + + Izabrane OU: {0} od {1} + Unesite nalog. @@ -1961,7 +1961,7 @@ Licencirano pod MIT licencom. Podrazumevana OU - Pronađi OU + Uvoz iz domena Prikazuje OU domena kojem je ovaj računar pridružen, koristeći vaš Windows nalog. Izaberite one koje želite da dodate. @@ -1976,7 +1976,7 @@ Licencirano pod MIT licencom. Dodaj izabrane OU - Unesite ime za prikaz i jedinstveno ime OU. OU mora pripadati gore navedenom domenu. + Unesite ime za prikaz i jedinstveno ime OU. OU mora pripadati konfigurisanom domenu. Ručno dodavanje OU @@ -1990,6 +1990,12 @@ Licencirano pod MIT licencom. Ime za prikaz + + Dodajte OU ručno ili uvezite OU iz domena kojem je ovaj računar pridružen. + + + Akcije OU + Lozinka diff --git a/src/Foundry/Strings/sv-SE/Resources.resw b/src/Foundry/Strings/sv-SE/Resources.resw index 55cb00df5..57a13c421 100644 --- a/src/Foundry/Strings/sv-SE/Resources.resw +++ b/src/Foundry/Strings/sv-SE/Resources.resw @@ -1840,8 +1840,8 @@ Licensierad under MIT-licensen. Sökningen avbröts. - - Sökningen är klar. Välj de OU:er som ska läggas till. + + Inga OU:er hittades i den här datorns domän. Endast vissa OU:er kunde visas. Du kan fortfarande lägga till de som visas. @@ -1858,12 +1858,12 @@ Licensierad under MIT-licensen. OU:erna lades inte till. De tillhör en annan domän än domännamnet eller de OU:er som redan finns i listan. - - De valda OU:erna har lagts till. - Ta bort markerade + + Valda OU:er: {0} av {1} + Ange kontot. @@ -1961,7 +1961,7 @@ Licensierad under MIT-licensen. Standard-OU - Sök efter OU:er + Importera från domän Visar OU:erna i den domän som den här datorn är ansluten till, med ditt Windows-konto. Välj de som ska läggas till. @@ -1976,7 +1976,7 @@ Licensierad under MIT-licensen. Lägg till valda OU:er - Ange ett visningsnamn och OU:ns unika namn. OU:n måste tillhöra domänen ovan. + Ange ett visningsnamn och OU:ns unika namn. OU:n måste tillhöra den konfigurerade domänen. Lägg till en OU manuellt @@ -1990,6 +1990,12 @@ Licensierad under MIT-licensen. Visningsnamn + + Lägg till en OU manuellt eller importera OU:er från den domän som den här datorn är ansluten till. + + + OU-åtgärder + Lösenord diff --git a/src/Foundry/Strings/th-TH/Resources.resw b/src/Foundry/Strings/th-TH/Resources.resw index fd655d75d..e2f85e6d5 100644 --- a/src/Foundry/Strings/th-TH/Resources.resw +++ b/src/Foundry/Strings/th-TH/Resources.resw @@ -1840,8 +1840,8 @@ ยกเลิกการค้นหาแล้ว - - ค้นหาเสร็จสมบูรณ์ เลือก OU ที่จะเพิ่ม + + ไม่พบ OU ในโดเมนของคอมพิวเตอร์เครื่องนี้ แสดงรายการ OU ได้เพียงบางส่วน คุณยังคงเพิ่ม OU ที่แสดงอยู่ได้ @@ -1858,12 +1858,12 @@ ไม่ได้เพิ่ม OU เนื่องจาก OU เหล่านี้อยู่ในโดเมนที่ต่างจากชื่อโดเมนหรือ OU ที่อยู่ในรายการแล้ว - - เพิ่ม OU ที่เลือกแล้ว - ลบที่เลือกไว้ + + OU ที่เลือก: {0} จาก {1} + ป้อนบัญชี @@ -1961,7 +1961,7 @@ OU เริ่มต้น - ค้นหา OU + นำเข้าจากโดเมน แสดงรายการ OU ของโดเมนที่คอมพิวเตอร์เครื่องนี้เข้าร่วมอยู่ โดยใช้บัญชี Windows ของคุณ เลือกรายการที่จะเพิ่ม @@ -1976,7 +1976,7 @@ เพิ่ม OU ที่เลือก - ป้อนชื่อที่แสดงและชื่อเฉพาะของ OU โดย OU ต้องอยู่ในโดเมนด้านบน + ป้อนชื่อที่แสดงและชื่อเฉพาะของ OU โดย OU ต้องอยู่ในโดเมนที่กำหนดค่าไว้ เพิ่ม OU ด้วยตนเอง @@ -1990,6 +1990,12 @@ ชื่อที่แสดง + + เพิ่ม OU ด้วยตนเองหรือนำเข้า OU จากโดเมนที่คอมพิวเตอร์เครื่องนี้เข้าร่วมอยู่ + + + การกระทำของ OU + รหัสผ่าน diff --git a/src/Foundry/Strings/tr-TR/Resources.resw b/src/Foundry/Strings/tr-TR/Resources.resw index 4236eb830..cecf7b01b 100644 --- a/src/Foundry/Strings/tr-TR/Resources.resw +++ b/src/Foundry/Strings/tr-TR/Resources.resw @@ -1840,8 +1840,8 @@ MIT Lisansı kapsamında lisanslanmıştır. Arama iptal edildi. - - Arama tamamlandı. Eklenecek OU’ları seçin. + + Bu bilgisayarın etki alanında OU bulunamadı. OU’ların yalnızca bir kısmı listelenebildi. Gösterilenleri yine de ekleyebilirsiniz. @@ -1858,12 +1858,12 @@ MIT Lisansı kapsamında lisanslanmıştır. OU’lar eklenmedi. Etki alanı adından veya listedeki OU’lardan farklı bir etki alanına aitler. - - Seçilen OU’lar eklendi. - Seçileni kaldır + + Seçilen OU’lar: {0} / {1} + Hesabı girin. @@ -1961,7 +1961,7 @@ MIT Lisansı kapsamında lisanslanmıştır. Varsayılan OU - OU’ları bul + Etki alanından içe aktar Windows hesabınızı kullanarak bu bilgisayarın katıldığı etki alanındaki OU’ları listeler. Eklenecek olanları seçin. @@ -1976,7 +1976,7 @@ MIT Lisansı kapsamında lisanslanmıştır. Seçilen OU’ları ekle - OU için bir görünen ad ve ayırt edici adı girin. OU yukarıdaki etki alanına ait olmalıdır. + OU için bir görünen ad ve ayırt edici adı girin. OU yapılandırılan etki alanına ait olmalıdır. El ile OU ekle @@ -1990,6 +1990,12 @@ MIT Lisansı kapsamında lisanslanmıştır. Görünen ad + + El ile bir OU ekleyin veya bu bilgisayarın katıldığı etki alanından OU’ları içe aktarın. + + + OU işlemleri + Parola diff --git a/src/Foundry/Strings/uk-UA/Resources.resw b/src/Foundry/Strings/uk-UA/Resources.resw index 520ae04c7..7032a3006 100644 --- a/src/Foundry/Strings/uk-UA/Resources.resw +++ b/src/Foundry/Strings/uk-UA/Resources.resw @@ -1840,8 +1840,8 @@ Пошук скасовано. - - Пошук завершено. Виберіть OU, які потрібно додати. + + У домені цього комп’ютера не знайдено жодного OU. Вдалося отримати лише частину OU. Показані OU все одно можна додати. @@ -1858,12 +1858,12 @@ OU не додано. Вони належать до іншого домену, ніж указане ім’я домену або OU, які вже є в списку. - - Вибрані OU додано. - Видалити вибране + + Вибрані OU: {0} з {1} + Введіть обліковий запис. @@ -1961,7 +1961,7 @@ OU за замовчуванням - Знайти OU + Імпорт із домену Показує OU домену, до якого приєднано цей комп’ютер, використовуючи ваш обліковий запис Windows. Виберіть ті, які потрібно додати. @@ -1976,7 +1976,7 @@ Додати вибрані OU - Введіть відображуване ім’я та розрізнювальне ім’я OU. OU має належати до домену, указаного вище. + Введіть відображуване ім’я та розрізнювальне ім’я OU. OU має належати до налаштованого домену. Додати OU вручну @@ -1990,6 +1990,12 @@ Відображуване ім’я + + Додайте OU вручну або імпортуйте OU з домену, до якого приєднано цей комп’ютер. + + + Дії з OU + Пароль diff --git a/src/Foundry/Strings/zh-CN/Resources.resw b/src/Foundry/Strings/zh-CN/Resources.resw index fe793892f..3b15c59cb 100644 --- a/src/Foundry/Strings/zh-CN/Resources.resw +++ b/src/Foundry/Strings/zh-CN/Resources.resw @@ -1840,8 +1840,8 @@ 搜索已取消。 - - 搜索完成。请选择要添加的 OU。 + + 在此计算机的域中未找到任何 OU。 只能列出部分 OU。您仍可添加已显示的 OU。 @@ -1858,12 +1858,12 @@ 未添加这些 OU。它们所属的域与域名或已列出的 OU 不一致。 - - 已添加所选 OU。 - 删除选定的 + + 已选择的 OU:{0} / {1} + 请输入账户。 @@ -1961,7 +1961,7 @@ 默认 OU - 查找 OU + 从域导入 使用您的 Windows 账户列出此计算机所加入的域中的 OU。请选择要添加的 OU。 @@ -1976,7 +1976,7 @@ 添加所选 OU - 请输入显示名称和 OU 的可分辨名称。OU 必须属于上方的域。 + 请输入显示名称和 OU 的可分辨名称。OU 必须属于已配置的域。 手动添加 OU @@ -1990,6 +1990,12 @@ 显示名称 + + 手动添加 OU,或从此计算机所加入的域导入 OU。 + + + OU 操作 + 密码 diff --git a/src/Foundry/Strings/zh-TW/Resources.resw b/src/Foundry/Strings/zh-TW/Resources.resw index 94cfd720b..2999aeb5d 100644 --- a/src/Foundry/Strings/zh-TW/Resources.resw +++ b/src/Foundry/Strings/zh-TW/Resources.resw @@ -1840,8 +1840,8 @@ 搜尋已取消。 - - 搜尋完成。請選取要新增的 OU。 + + 在此電腦的網域中找不到任何 OU。 只能列出部分 OU。您仍可新增已顯示的 OU。 @@ -1858,12 +1858,12 @@ 未新增這些 OU。它們所屬的網域與網域名稱或已列出的 OU 不一致。 - - 已新增選取的 OU。 - 刪除選定的 + + 已選取的 OU:{0} / {1} + 請輸入帳戶。 @@ -1961,7 +1961,7 @@ 預設 OU - 尋找 OU + 從網域匯入 使用您的 Windows 帳戶列出此電腦所加入網域中的 OU。請選取要新增的 OU。 @@ -1976,7 +1976,7 @@ 新增選取的 OU - 請輸入顯示名稱和 OU 的辨別名稱。OU 必須屬於上方的網域。 + 請輸入顯示名稱和 OU 的辨別名稱。OU 必須屬於已設定的網域。 手動新增 OU @@ -1990,6 +1990,12 @@ 顯示名稱 + + 手動新增 OU,或從此電腦所加入的網域匯入 OU。 + + + OU 操作 + 密碼 diff --git a/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs b/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs index 66b940716..9ea75d8a4 100644 --- a/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs +++ b/src/Foundry/ViewModels/DomainJoinConfigurationViewModel.cs @@ -12,34 +12,32 @@ namespace Foundry.ViewModels; -/// Shares domain metadata, volatile credentials and explicit OU search/import across both authoring pages. +/// Shares domain metadata, volatile credentials and the OU list, with its add and import dialogs, across both authoring pages. public sealed partial class DomainJoinConfigurationViewModel : ObservableObject, IDisposable { private readonly IFoundryConfigurationStateService configuration; private readonly IDomainJoinSecretStateService secrets; private readonly IDeploymentProtectionSecretStateService protectionSecrets; private readonly IAuthoringDomainOuDiscoveryService discovery; + private readonly IDomainJoinOuDialogService ouDialogs; private readonly IDialogService dialogs; private readonly IApplicationLocalizationService localization; private readonly List selectedListedRows = []; - private readonly List selectedPreviewRows = []; private IReadOnlyList issues = []; private CancellationTokenSource? discoveryCancellation; private DomainJoinSettings? appliedSettings; - private DomainJoinSettings? previewBaseline; - private DomainOuDiscoveryResult? preview; private DomainJoinMode pageMode; private bool applying; private bool disposed; private bool credentialInputInvalid; private long discoveryRevision; - private string? listStatusKey; - private string? discoveryStatusKey; + private string? statusKey; public DomainJoinConfigurationViewModel(IFoundryConfigurationStateService configuration, IDomainJoinSecretStateService secrets, IDeploymentProtectionSecretStateService protectionSecrets, IAuthoringDomainOuDiscoveryService discovery, - IDialogService dialogs, IApplicationLocalizationService localization) + IDomainJoinOuDialogService ouDialogs, IDialogService dialogs, IApplicationLocalizationService localization) { + this.ouDialogs = ouDialogs; this.configuration = configuration; this.secrets = secrets; this.protectionSecrets = protectionSecrets; @@ -53,10 +51,8 @@ public DomainJoinConfigurationViewModel(IFoundryConfigurationStateService config ApplyState(); } - /// Gets the saved OUs; a search does not change this collection until selected results are added. + /// Gets the saved OUs; a domain search changes this collection only through the import dialog. public ObservableCollection OrganizationalUnits { get; } = []; - /// Gets the bounded read-only search results; only rows selected in the table are added. - public ObservableCollection PreviewUnits { get; } = []; public bool IsActive => configuration.Current.DomainJoin.IsEnabled && configuration.Current.DomainJoin.Mode == pageMode; public string ActionText => localization.GetString(IsActive ? "Common.Disable" : "Common.Enable"); public string DocumentationUrl => pageMode == DomainJoinMode.Interactive @@ -66,10 +62,10 @@ public DomainJoinConfigurationViewModel(IFoundryConfigurationStateService config public string DistinguishedNameColumnHeader => localization.GetString("DomainJoinManualDn.Header"); public string RemoveSelectedText => Text("RemoveSelected"); public string EmptyListText => Text("EmptyCatalog"); - /// Opens the organizational units section on load only when OUs are already listed. - public bool HasOrganizationalUnits => OrganizationalUnits.Count > 0; - public Visibility ListVisibility => ToVisibility(HasOrganizationalUnits); - public Visibility EmptyListVisibility => ToVisibility(!HasOrganizationalUnits); + /// Gets the import button label; the same button cancels a running domain search. + public string ImportButtonText => localization.GetString(IsDiscovering ? "DomainJoinCancel.Content" : "DomainJoinDiscover.Content"); + public Visibility ListVisibility => ToVisibility(OrganizationalUnits.Count > 0); + public Visibility EmptyListVisibility => ToVisibility(OrganizationalUnits.Count == 0); public string DomainValidationMessage => GetIssueText(IsDomainIssue); public Visibility DomainValidationVisibility => ToVisibility(DomainValidationMessage.Length > 0); @@ -77,16 +73,11 @@ public DomainJoinConfigurationViewModel(IFoundryConfigurationStateService config public Visibility CredentialsValidationVisibility => ToVisibility(CredentialsValidationMessage.Length > 0); public string OrganizationalUnitsValidationMessage => GetIssueText(code => !IsDomainIssue(code) && !IsCredentialIssue(code)); public Visibility OrganizationalUnitsValidationVisibility => ToVisibility(OrganizationalUnitsValidationMessage.Length > 0); - public string ListStatusText => listStatusKey is null ? string.Empty : Text(listStatusKey); - public Visibility ListStatusVisibility => ToVisibility(listStatusKey is not null); + /// Gets the progress or outcome of the last domain search, shown beside the OU actions. + public string StatusText => statusKey is null ? string.Empty : Text(statusKey); + public Visibility StatusVisibility => ToVisibility(statusKey is not null); + public Visibility DiscoveringVisibility => ToVisibility(IsDiscovering); - public string DiscoveryStatusText => discoveryStatusKey is null ? string.Empty : Text(discoveryStatusKey); - public Visibility DiscoveryStatusVisibility => ToVisibility(discoveryStatusKey is not null); - public string PreviewDomainText => preview?.ComputerDomain ?? string.Empty; - public Visibility PreviewVisibility => ToVisibility(PreviewUnits.Count > 0); - - private bool CanDiscover => !IsDiscovering && IsActive; - private bool CanImport => preview is not null && selectedPreviewRows.Count > 0 && ReferenceEquals(previewBaseline, configuration.Current.DomainJoin); private bool CanRemoveSelected => selectedListedRows.Count > 0; [ObservableProperty] @@ -100,10 +91,6 @@ public DomainJoinConfigurationViewModel(IFoundryConfigurationStateService config [ObservableProperty] public partial DomainJoinOrganizationalUnitEntryViewModel? SelectedDefaultOu { get; set; } [ObservableProperty] - public partial string ManualDisplayName { get; set; } = string.Empty; - [ObservableProperty] - public partial string ManualDistinguishedName { get; set; } = string.Empty; - [ObservableProperty] public partial bool IsDiscovering { get; set; } /// Invalidates PasswordBox content when volatile secret ownership or profile state changes. [ObservableProperty] @@ -159,20 +146,22 @@ public void SetPassword(ReadOnlySpan password) } [RelayCommand] - private void AddOrganizationalUnit() + private Task AddOrganizationalUnitAsync() => ouDialogs.ShowAddAsync(TryAddOrganizationalUnit); + + /// Adds one typed OU; returns the reason shown in the dialog when the entry is refused. + private string? TryAddOrganizationalUnit(string displayName, string distinguishedName) { + if (disposed) return null; DomainJoinSettings current = configuration.Current.DomainJoin; var unit = new DomainJoinOrganizationalUnitSettings - { Id = Guid.NewGuid().ToString("D"), DisplayName = ManualDisplayName.Trim(), DistinguishedName = ManualDistinguishedName.Trim() }; + { Id = Guid.NewGuid().ToString("D"), DisplayName = displayName.Trim(), DistinguishedName = distinguishedName.Trim() }; DomainJoinOrganizationalUnitSettings[] added = [unit]; try { Save(DomainJoinOrganizationalUnitCatalog.Merge(current, current.DomainName ?? string.Empty, added)); - ManualDisplayName = string.Empty; - ManualDistinguishedName = string.Empty; - SetListStatus(null); + return null; } - catch (ArgumentException) { SetListStatus(GetMergeFailureKey(current, added, "CatalogInputInvalid")); } + catch (ArgumentException) { return Text(GetMergeFailureKey(current, added, "CatalogInputInvalid")); } } /// Tracks the saved-OU table selection that acts on. @@ -183,13 +172,6 @@ public void ReplaceSelectedListedRows(IEnumerableTracks the search-result selection; selection alone never saves an OU. - public void ReplaceSelectedPreviewRows(IEnumerable rows) - { - selectedPreviewRows.Clear(); - selectedPreviewRows.AddRange(rows); - ImportSelectedCommand.NotifyCanExecuteChanged(); - } /// Removes the selected saved OUs and a default among them, keeping the others. [RelayCommand(CanExecute = nameof(CanRemoveSelected))] @@ -207,32 +189,52 @@ private void RemoveSelected() [RelayCommand] private void ClearDefault() => SelectedDefaultOu = null; - [RelayCommand(CanExecute = nameof(CanDiscover))] - private async Task DiscoverAsync() + /// + /// Searches the authoring computer's domain, then lets the user pick the OUs to add. Invoked again while the + /// search runs, it cancels that search instead. + /// + [RelayCommand(AllowConcurrentExecutions = true)] + private async Task ImportFromDomainAsync() { - DropPreview(); - long revision = ++discoveryRevision; + if (IsDiscovering) + { + StopDiscovery(); + SetStatus("DiscoveryCanceled"); + return; + } + DomainJoinSettings baseline = configuration.Current.DomainJoin; + DomainOuDiscoveryResult? result = await SearchDomainAsync(baseline); + if (result is null) return; + string? failureKey = result.Status switch + { + DomainOuDiscoveryStatus.Canceled => "DiscoveryCanceled", + DomainOuDiscoveryStatus.Unavailable => result.ErrorCode == "Timeout" ? "DiscoveryTimeout" : "DiscoveryUnavailable", + _ => result.Candidates.Count == 0 ? "DiscoveryEmpty" : null + }; + SetStatus(failureKey); + if (failureKey is not null) return; + + IReadOnlyList? selected = + await ouDialogs.PickAsync(result.Candidates, result.Status == DomainOuDiscoveryStatus.Incomplete); + // The settings can change while the dialog is open, for example through profile synchronization. + if (selected is null || selected.Count == 0 || disposed || !ReferenceEquals(baseline, configuration.Current.DomainJoin)) return; + try { Save(DomainJoinOrganizationalUnitCatalog.Merge(baseline, result.ComputerDomain ?? string.Empty, selected)); } + catch (ArgumentException) { SetStatus(GetMergeFailureKey(baseline, selected, "ImportDomainMismatch")); } + } + + /// Runs one search; returns null when it was canceled, superseded or made stale by a settings change. + private async Task SearchDomainAsync(DomainJoinSettings baseline) + { + long revision = ++discoveryRevision; var cancellation = new CancellationTokenSource(); discoveryCancellation = cancellation; IsDiscovering = true; - SetDiscoveryStatus("Discovering"); - RefreshPresentation(); + SetStatus("Discovering"); try { DomainOuDiscoveryResult result = await discovery.DiscoverAsync(cancellation.Token); - if (disposed || revision != discoveryRevision || !ReferenceEquals(baseline, configuration.Current.DomainJoin)) return; - preview = result; - previewBaseline = baseline; - foreach (DomainJoinOrganizationalUnitSettings unit in result.Candidates.OrderBy(unit => unit.DistinguishedName, StringComparer.OrdinalIgnoreCase)) - PreviewUnits.Add(new(unit)); - SetDiscoveryStatus(result.Status switch - { - DomainOuDiscoveryStatus.Complete => "DiscoveryComplete", - DomainOuDiscoveryStatus.Incomplete => "DiscoveryIncomplete", - DomainOuDiscoveryStatus.Canceled => "DiscoveryCanceled", - _ => result.ErrorCode == "Timeout" ? "DiscoveryTimeout" : "DiscoveryUnavailable" - }); + return !disposed && revision == discoveryRevision && ReferenceEquals(baseline, configuration.Current.DomainJoin) ? result : null; } finally { @@ -240,46 +242,19 @@ private async Task DiscoverAsync() { IsDiscovering = false; discoveryCancellation = null; - RefreshPresentation(); } cancellation.Dispose(); } } - [RelayCommand(CanExecute = nameof(IsDiscovering))] - private void CancelDiscovery() - { - DropPreview(); - SetDiscoveryStatus("DiscoveryCanceled"); - } - - [RelayCommand(CanExecute = nameof(CanImport))] - private void ImportSelected() - { - if (preview is null || !ReferenceEquals(previewBaseline, configuration.Current.DomainJoin)) return; - DomainJoinSettings current = configuration.Current.DomainJoin; - DomainJoinOrganizationalUnitSettings[] selected = selectedPreviewRows.Select(row => row.Settings).ToArray(); - try - { - Save(DomainJoinOrganizationalUnitCatalog.Merge(current, preview.ComputerDomain ?? string.Empty, selected)); - SetDiscoveryStatus("Imported"); - } - catch (ArgumentException) { SetDiscoveryStatus(GetMergeFailureKey(current, selected, "ImportDomainMismatch")); } - } - - /// Stops a running search and discards its results and status, because they describe an earlier state. - private void DropPreview() + /// Stops a running search and clears its status, because both describe an earlier state. + private void StopDiscovery() { discoveryRevision++; discoveryCancellation?.Cancel(); discoveryCancellation = null; IsDiscovering = false; - preview = null; - previewBaseline = null; - selectedPreviewRows.Clear(); - PreviewUnits.Clear(); - SetDiscoveryStatus(null); - RefreshPresentation(); + SetStatus(null); } /// Names the specific reason a merge was refused, falling back to the caller's general message. @@ -293,9 +268,11 @@ private static string GetMergeFailureKey(DomainJoinSettings current, IReadOnlyLi return reusesListedId ? "Validation." + DomainJoinValidationCode.DuplicateOuId : fallbackKey; } - partial void OnIsDiscoveringChanged(bool value) => CancelDiscoveryCommand.NotifyCanExecuteChanged(); - partial void OnManualDisplayNameChanged(string value) => SetListStatus(null); - partial void OnManualDistinguishedNameChanged(string value) => SetListStatus(null); + partial void OnIsDiscoveringChanged(bool value) + { + OnPropertyChanged(nameof(ImportButtonText)); + OnPropertyChanged(nameof(DiscoveringVisibility)); + } partial void OnDomainNameChanged(string value) { @@ -347,23 +324,16 @@ private void SetCredentialInputInvalid(bool value) OnPropertyChanged(nameof(CredentialsValidationVisibility)); } - private void SetListStatus(string? key) - { - listStatusKey = key; - OnPropertyChanged(nameof(ListStatusText)); - OnPropertyChanged(nameof(ListStatusVisibility)); - } - - private void SetDiscoveryStatus(string? key) + private void SetStatus(string? key) { - discoveryStatusKey = key; - OnPropertyChanged(nameof(DiscoveryStatusText)); - OnPropertyChanged(nameof(DiscoveryStatusVisibility)); + statusKey = key; + OnPropertyChanged(nameof(StatusText)); + OnPropertyChanged(nameof(StatusVisibility)); } /// /// Rebuilds the page only when the Domain Join settings themselves changed. Unrelated configuration changes - /// reuse the same settings instance and must not discard a running search, its results or table selections. + /// reuse the same settings instance and must not discard a running search or the table selection. /// private void OnStateChanged(object? sender, EventArgs args) { @@ -373,7 +343,7 @@ private void OnStateChanged(object? sender, EventArgs args) return; } - DropPreview(); + StopDiscovery(); ApplyState(); } @@ -391,8 +361,8 @@ private void OnLanguageChanged(object? sender, ApplicationLanguageChangedEventAr OnPropertyChanged(nameof(DistinguishedNameColumnHeader)); OnPropertyChanged(nameof(RemoveSelectedText)); OnPropertyChanged(nameof(EmptyListText)); - OnPropertyChanged(nameof(ListStatusText)); - OnPropertyChanged(nameof(DiscoveryStatusText)); + OnPropertyChanged(nameof(ImportButtonText)); + OnPropertyChanged(nameof(StatusText)); RefreshPresentation(); } @@ -443,10 +413,6 @@ private void RefreshPresentation() OnPropertyChanged(nameof(DocumentationUrl)); OnPropertyChanged(nameof(ListVisibility)); OnPropertyChanged(nameof(EmptyListVisibility)); - OnPropertyChanged(nameof(PreviewDomainText)); - OnPropertyChanged(nameof(PreviewVisibility)); - DiscoverCommand.NotifyCanExecuteChanged(); - ImportSelectedCommand.NotifyCanExecuteChanged(); RemoveSelectedCommand.NotifyCanExecuteChanged(); } @@ -454,7 +420,7 @@ private void RefreshPresentation() public void Dispose() { disposed = true; - DropPreview(); + StopDiscovery(); configuration.StateChanged -= OnStateChanged; secrets.Changed -= OnSecretsChanged; protectionSecrets.Changed -= OnProtectionSecretsChanged; diff --git a/src/Foundry/ViewModels/DomainJoinOuSelectionDialogViewModel.cs b/src/Foundry/ViewModels/DomainJoinOuSelectionDialogViewModel.cs new file mode 100644 index 000000000..08145419f --- /dev/null +++ b/src/Foundry/ViewModels/DomainJoinOuSelectionDialogViewModel.cs @@ -0,0 +1,113 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using System.ComponentModel; +using Foundry.Core.Models.Configuration; +using Foundry.Services.Localization; + +namespace Foundry.ViewModels; + +/// Backs the picker shown after searching the authoring computer's domain for organizational units. +public sealed partial class DomainJoinOuSelectionDialogViewModel : ObservableObject, IDisposable +{ + private readonly IApplicationLocalizationService localization; + + public DomainJoinOuSelectionDialogViewModel(IApplicationLocalizationService localization, + IReadOnlyList candidates, bool isIncomplete) + { + this.localization = localization; + ArgumentNullException.ThrowIfNull(candidates); + + Title = localization.GetString("DomainJoinDiscovery.Header"); + Description = localization.GetString("DomainJoinDiscovery.Description"); + IncompleteText = isIncomplete ? localization.GetString("DomainJoin.DiscoveryIncomplete") : string.Empty; + SelectAllText = localization.GetString("Autopilot.TenantPickerSelectAll"); + ClearText = localization.GetString("Autopilot.TenantPickerClear"); + AddText = localization.GetString("DomainJoinImportSelected.Content"); + CancelText = localization.GetString("Common.Cancel"); + foreach (DomainJoinOrganizationalUnitSettings unit in candidates.OrderBy(unit => unit.DistinguishedName, StringComparer.OrdinalIgnoreCase)) + { + var entry = new SelectableDomainJoinOuEntryViewModel(unit); + entry.PropertyChanged += OnEntryPropertyChanged; + OrganizationalUnits.Add(entry); + } + + RefreshSelectionState(); + } + + /// Gets the discovered OUs with per-row selection state. + public ObservableCollection OrganizationalUnits { get; } = []; + public string Title { get; } + public string Description { get; } + /// Gets the notice shown when the directory returned only part of its OUs; empty otherwise. + public string IncompleteText { get; } + public Visibility IncompleteVisibility => IncompleteText.Length > 0 ? Visibility.Visible : Visibility.Collapsed; + public string SelectAllText { get; } + public string ClearText { get; } + public string AddText { get; } + public string CancelText { get; } + + [ObservableProperty] + public partial string SelectedCountDisplay { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool HasSelection { get; set; } + + /// Gets the OUs checked by the user. + public IReadOnlyList GetSelected() => + OrganizationalUnits.Where(entry => entry.IsSelected).Select(entry => entry.Settings).ToArray(); + + /// Releases row selection subscriptions. + public void Dispose() + { + foreach (SelectableDomainJoinOuEntryViewModel entry in OrganizationalUnits) + { + entry.PropertyChanged -= OnEntryPropertyChanged; + } + } + + [RelayCommand] + private void SelectAll() => SetAll(true); + + [RelayCommand] + private void Clear() => SetAll(false); + + private void SetAll(bool isSelected) + { + foreach (SelectableDomainJoinOuEntryViewModel entry in OrganizationalUnits) + { + entry.IsSelected = isSelected; + } + } + + private void RefreshSelectionState() + { + int selectedCount = OrganizationalUnits.Count(entry => entry.IsSelected); + HasSelection = selectedCount > 0; + SelectedCountDisplay = localization.FormatString("DomainJoin.SelectedCountFormat", selectedCount, OrganizationalUnits.Count); + } + + private void OnEntryPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.Equals(e.PropertyName, nameof(SelectableDomainJoinOuEntryViewModel.IsSelected), StringComparison.Ordinal)) + { + RefreshSelectionState(); + } + } +} + +/// +/// Wraps a discovered OU with dialog selection state. Rows start unchecked because a domain lists OUs, such as +/// Domain Controllers, that must not receive deployed computers by default. +/// +public sealed partial class SelectableDomainJoinOuEntryViewModel(DomainJoinOrganizationalUnitSettings settings) : ObservableObject +{ + public DomainJoinOrganizationalUnitSettings Settings { get; } = settings ?? throw new ArgumentNullException(nameof(settings)); + public string DisplayName => Settings.DisplayName; + public string DistinguishedName => Settings.DistinguishedName; + + [ObservableProperty] + public partial bool IsSelected { get; set; } +} diff --git a/src/Foundry/Views/DomainJoinOuAddDialog.xaml b/src/Foundry/Views/DomainJoinOuAddDialog.xaml new file mode 100644 index 000000000..99f7a4587 --- /dev/null +++ b/src/Foundry/Views/DomainJoinOuAddDialog.xaml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/src/Foundry/Views/DomainJoinOuAddDialog.xaml.cs b/src/Foundry/Views/DomainJoinOuAddDialog.xaml.cs new file mode 100644 index 000000000..a7ba9a388 --- /dev/null +++ b/src/Foundry/Views/DomainJoinOuAddDialog.xaml.cs @@ -0,0 +1,32 @@ +// Copyright (c) Foundry Project contributors. +// Licensed under the MIT License. +// See the LICENSE file in the project root for more information. + +using Foundry.Services.Localization; + +namespace Foundry.Views; + +/// Collects one organizational unit and stays open until the owning page accepts it. +public sealed partial class DomainJoinOuAddDialog : ContentDialog +{ + private readonly Func tryAdd; + + public DomainJoinOuAddDialog(IApplicationLocalizationService localization, Func tryAdd) + { + ArgumentNullException.ThrowIfNull(localization); + this.tryAdd = tryAdd ?? throw new ArgumentNullException(nameof(tryAdd)); + InitializeComponent(); + Title = localization.GetString("DomainJoinManualDestination.Header"); + DescriptionTextBlock.Text = localization.GetString("DomainJoinManualDestination.Description"); + PrimaryButtonText = localization.GetString("DomainJoinAdd.Content"); + CloseButtonText = localization.GetString("Common.Cancel"); + } + + private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + string? refusal = tryAdd(DisplayNameBox.Text, DistinguishedNameBox.Text); + ValidationTextBlock.Text = refusal ?? string.Empty; + ValidationTextBlock.Visibility = refusal is null ? Visibility.Collapsed : Visibility.Visible; + args.Cancel = refusal is not null; + } +} diff --git a/src/Foundry/Views/DomainJoinOuSelectionDialog.xaml b/src/Foundry/Views/DomainJoinOuSelectionDialog.xaml new file mode 100644 index 000000000..18707d1fb --- /dev/null +++ b/src/Foundry/Views/DomainJoinOuSelectionDialog.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + +