Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Bicep.Core.UnitTests/BicepTestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,13 @@ public static IArtifactRegistryProvider CreateRegistryProvider(IServiceProvider
var transportFactory = new OciRegistryTransportFactory(transport, dockerCredentials);
var publicMetadataProvider = (services.GetService(typeof(IPublicModuleMetadataProvider)) as IPublicModuleMetadataProvider)
?? StrictMock.Of<IPublicModuleMetadataProvider>().Object;
return new DefaultArtifactRegistryProvider(TestRegistryConfiguration, transportFactory, publicMetadataProvider, TemplateSpecRepositoryFactory, FileExplorer);
return new DefaultArtifactRegistryProvider(TestRegistryConfiguration, TestCloudConfigurationTrustPolicy, transportFactory, publicMetadataProvider, TemplateSpecRepositoryFactory, FileExplorer);
}

public static readonly RegistryConfiguration TestRegistryConfiguration = new(PermitUntrustedRegistries: true);

public static readonly CloudConfigurationTrustPolicy TestCloudConfigurationTrustPolicy = new();

public static IModuleDispatcher CreateModuleDispatcher(IServiceProvider services) => new ModuleDispatcher(CreateRegistryProvider(services));

public static readonly NamespaceResolver DefaultNamespaceResolver = NamespaceResolver.Create([
Expand Down Expand Up @@ -152,7 +154,7 @@ public static IBicepConfiguration CreateMockConfiguration(Dictionary<string, obj
public static BicepConfigurationManager CreateFilesystemConfigurationManager()
{
var fileExplorer = new FileSystemFileExplorer(new OnDiskFileSystem());
return new BicepConfigurationManager(fileExplorer);
return new BicepConfigurationManager(fileExplorer, TestCloudConfigurationTrustPolicy);
}

public static IFeatureProviderFactory CreateFeatureProviderFactory(FeatureProviderOverrides featureOverrides, IBicepConfigurationManager? configurationManager = null)
Expand Down
130 changes: 113 additions & 17 deletions src/Bicep.Core.UnitTests/Configuration/BicepConfigurationManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class BicepConfigurationManagerTests

private static IBicepConfigurationChain GetChain(TestFileSet fileSet, string sourceFile = "main.bicep")
{
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
return sut.GetConfigurationChain(fileSet.GetUri(sourceFile));
}

Expand Down Expand Up @@ -64,6 +64,102 @@ public void GetConfigurationChain_SingleConfigNoExtends_ReturnsConfigWithNoError
chain.GetEffectiveConfiguration().GetDiagnostics().Should().BeEmpty();
}

[DataTestMethod]
[DataRow("AzureBleuCloud")]
[DataRow("AzureUSGovernment")]
public void GetConfigurationChain_CanonicalCloudProfileOverride_IsTrusted(string profileName)
{
var fileSet = InMemoryTestFileSet.Create(
("main.bicep", ""),
("bicepconfig.json", $$"""
{
"cloud": {
"currentProfile": "{{profileName}}"
}
}
"""));

GetChain(fileSet).GetEffectiveConfiguration().GetDiagnostics().Should().BeEmpty();
}

[TestMethod]
public void GetConfigurationChain_CustomCloudProfile_ReportsUntrustedProfile()
{
var fileSet = InMemoryTestFileSet.Create(
("main.bicep", ""),
("bicepconfig.json", """
{
"cloud": {
"currentProfile": "Custom",
"profiles": {
"Custom": {
"resourceManagerEndpoint": "https://management.example.invalid",
"activeDirectoryAuthority": "https://login.example.invalid"
}
}
}
}
"""));

GetChain(fileSet).GetEffectiveConfiguration().GetDiagnostics()
.Should().ContainSingle(diagnostic => diagnostic.Code == "BCP456");
}

[TestMethod]
public void GetConfigurationChain_RepositoryEnvironmentCredential_DoesNotRequireSeparateTrust()
{
var fileSet = InMemoryTestFileSet.Create(
("main.bicep", ""),
("bicepconfig.json", """{ "cloud": { "credentialPrecedence": ["Environment"] } }"""));

GetChain(fileSet).GetEffectiveConfiguration().GetDiagnostics()
.Should().BeEmpty();
}

[TestMethod]
public void GetConfigurationChain_ExactExternalTrustApprovesCustomProfile()
{
var fileSet = InMemoryTestFileSet.Create(
("main.bicep", ""),
("bicepconfig.json", """
{
"cloud": {
"currentProfile": "Custom",
"profiles": {
"Custom": {
"resourceManagerEndpoint": "https://management.example.invalid",
"activeDirectoryAuthority": "https://login.example.invalid"
}
},
"credentialPrecedence": ["Environment"]
}
}
"""));
var trustPolicy = CloudConfigurationTrustPolicy.FromEnvironmentValue("""
[{
"resourceManagerEndpoint": "https://management.example.invalid",
"activeDirectoryAuthority": "https://login.example.invalid"
}]
""");
var sut = new BicepConfigurationManager(fileSet.FileExplorer, trustPolicy);

sut.GetConfigurationChain(fileSet.GetUri("main.bicep"))
.GetEffectiveConfiguration().GetDiagnostics().Should().BeEmpty();
}

[TestMethod]
public void GetConfigurationChain_MalformedExternalTrust_DoesNotBlockBuiltInCloud()
{
var fileSet = InMemoryTestFileSet.Create(("main.bicep", ""));
var sut = new BicepConfigurationManager(
fileSet.FileExplorer,
CloudConfigurationTrustPolicy.FromEnvironmentValue("*"));

sut.GetConfigurationChain(fileSet.GetUri("main.bicep"))
.GetEffectiveConfiguration().GetDiagnostics()
.Should().BeEmpty();
}

// ── Simple two-level extends ──────────────────────────────────────────

[TestMethod]
Expand Down Expand Up @@ -151,7 +247,7 @@ public void GetConfigurationChain_FullConfigMerge_LeafWinsAndBaseInherited()
}
"""));

var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

// Act — load chain once.
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
Expand Down Expand Up @@ -304,7 +400,7 @@ public void GetConfigurationChain_NonFileUri_ReturnsBuiltInChain()
{
// Arrange — source file is a remote URI (e.g. from a registry).
var fileSet = InMemoryTestFileSet.Create();
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
var remoteUri = new IOUri(new IOUriScheme("https"), "management.azure.com", "/bicep/main.bicep");

// Act.
Expand All @@ -325,7 +421,7 @@ public void GetConfigurationChain_CalledTwiceForSameSource_ReturnsSameChainInsta
("main.bicep", ""),
("bicepconfig.json", """{ "experimentalFeaturesWarning": true }"""));

var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

// Act.
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
Expand All @@ -343,7 +439,7 @@ public void GetConfigurationChain_AfterPurgeCache_ReturnsNewInstance()
("main.bicep", ""),
("bicepconfig.json", """{ "experimentalFeaturesWarning": true }"""));

var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

// Act.
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
Expand All @@ -365,7 +461,7 @@ public void PurgeCacheForAffectedChains_AfterBaseFileContentChanges_PicksUpNewVa
("bicepconfig.json", """{ "extends": "./base/bicepconfig.base.json" }"""),
("base/bicepconfig.base.json", """{ "experimentalFeaturesWarning": false }"""));

var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

// Load chain — should reflect base value (false).
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
Expand All @@ -392,7 +488,7 @@ public void GetDependenciesForLeaf_SingleConfig_TracksSelf()
var fileSet = InMemoryTestFileSet.Create(
("main.bicep", ""),
("bicepconfig.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

var deps = sut.GetDependenciesForLeaf(fileSet.GetUri("bicepconfig.json"));
Expand All @@ -407,7 +503,7 @@ public void GetDependenciesForLeaf_LeafExtendsBase_TracksBothFiles()
("main.bicep", ""),
("bicepconfig.json", """{ "extends": "./base/bicepconfig.base.json" }"""),
("base/bicepconfig.base.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

var deps = sut.GetDependenciesForLeaf(fileSet.GetUri("bicepconfig.json"));
Expand All @@ -425,7 +521,7 @@ public void GetDependenciesForLeaf_DeepChain_TracksAllFiles()
("bicepconfig.json", """{ "extends": "./b/bicepconfig.b.json" }"""),
("b/bicepconfig.b.json", """{ "extends": "../c/bicepconfig.c.json" }"""),
("c/bicepconfig.c.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

var deps = sut.GetDependenciesForLeaf(fileSet.GetUri("bicepconfig.json"));
Expand All @@ -445,7 +541,7 @@ public void PurgeCacheForAffectedChains_BaseFileChanges_InvalidatesAffectedChain
("main.bicep", ""),
("bicepconfig.json", """{ "extends": "./base/bicepconfig.base.json" }"""),
("base/bicepconfig.base.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

// Verify chain1 has correct content before purge.
Expand All @@ -469,7 +565,7 @@ public void PurgeCacheForAffectedChains_UnrelatedFileChanges_DoesNotInvalidateCh
("bicepconfig.json", """{ "experimentalFeaturesWarning": true }"""),
("other/other.bicep", ""),
("other/bicepconfig.json", """{ "experimentalFeaturesWarning": false }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

// Verify chain1 has correct content before purge.
Expand All @@ -490,7 +586,7 @@ public void PurgeCacheForAffectedChains_LeafFileChanges_InvalidatesItsOwnChain()
var fileSet = InMemoryTestFileSet.Create(
("main.bicep", ""),
("bicepconfig.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

// Verify chain1 has correct content before purge.
Expand All @@ -514,7 +610,7 @@ public void PurgeCacheForAffectedChains_DeepChainBaseChanges_InvalidatesEntireCh
("bicepconfig.json", """{ "extends": "./b/bicepconfig.b.json" }"""),
("b/bicepconfig.b.json", """{ "extends": "../c/bicepconfig.c.json" }"""),
("c/bicepconfig.c.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
var chain1 = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));

// Verify chain1 has correct content — value inherited from deepest C.
Expand All @@ -539,7 +635,7 @@ public void PurgeCacheForAffectedChains_SharedBase_InvalidatesOnlyAffectedLeaf()
("other/other.bicep", ""),
("other/bicepconfig.json", """{ "experimentalFeaturesWarning": false }"""),
("shared/bicepconfig.shared.json", """{ "experimentalFeaturesWarning": true }"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);
var chainMain = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
var chainOther = sut.GetConfigurationChain(fileSet.GetUri("other/other.bicep"));

Expand Down Expand Up @@ -579,7 +675,7 @@ public void GetConfigurationChain_AliasInLeaf_DeclaringConfigUriIsLeaf()
}
}
"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

var chain = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
var alias = chain.GetEffectiveConfiguration().ModuleAliasesMock
Expand All @@ -605,7 +701,7 @@ public void GetConfigurationChain_AliasInBase_DeclaringConfigUriIsBase()
}
}
"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

var chain = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
var alias = chain.GetEffectiveConfiguration().ModuleAliasesMock
Expand Down Expand Up @@ -641,7 +737,7 @@ public void GetConfigurationChain_AliasOverriddenInLeaf_DeclaringConfigUriIsLeaf
}
}
"""));
var sut = new BicepConfigurationManager(fileSet.FileExplorer);
var sut = new BicepConfigurationManager(fileSet.FileExplorer, BicepTestConstants.TestCloudConfigurationTrustPolicy);

var chain = sut.GetConfigurationChain(fileSet.GetUri("main.bicep"));
var alias = chain.GetEffectiveConfiguration().ModuleAliasesMock
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Immutable;
using Bicep.Core.Configuration;
using Bicep.Core.Json;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.Core.UnitTests.Configuration;

[TestClass]
public class CloudConfigurationTrustPolicyRegistrationTests
{
private const string CustomCloudTrustJson = """
[{
"resourceManagerEndpoint": "https://management.example.invalid",
"activeDirectoryAuthority": "https://login.example.invalid"
}]
""";

[TestMethod]
public void TrustPolicyIsBuiltFromTheTrustedCloudsEnvironmentVariable()
{
var policy = BuildPolicy((CloudConfigurationTrustPolicy.TrustedCloudsEnvironmentVariable, CustomCloudTrustJson));

policy.IsTrusted(CreateCustomCloud("https://management.example.invalid", "https://login.example.invalid")).Should().BeTrue();
policy.IsTrusted(CreateCustomCloud("https://management.example.invalid", "https://login.other.invalid")).Should().BeFalse();
}

[TestMethod]
[DataRow("BICEP_TRUSTED_CLOUD")]
[DataRow("BICEP_TRUSTED_CLOUDS_")]
[DataRow("bicep_trusted_clouds")]
public void MisspelledEnvironmentVariableGrantsNoTrust(string variableName)
{
var policy = BuildPolicy((variableName, CustomCloudTrustJson));

policy.IsTrusted(CreateCustomCloud("https://management.example.invalid", "https://login.example.invalid")).Should().BeFalse();
policy.IsTrusted((CloudConfiguration)BicepConfiguration.BuiltIn.Cloud).Should().BeTrue();
}

[TestMethod]
public void UnsetEnvironmentVariableGrantsNoAdditionalTrust()
{
var policy = BuildPolicy();

policy.IsTrusted(CreateCustomCloud("https://management.example.invalid", "https://login.example.invalid")).Should().BeFalse();
policy.IsTrusted((CloudConfiguration)BicepConfiguration.BuiltIn.Cloud).Should().BeTrue();
}

private static CloudConfigurationTrustPolicy BuildPolicy(params (string key, string? value)[] variables)
=> new ServiceBuilder()
.WithRegistration(services => services.WithEnvironmentVariables(variables))
.Build()
.Construct<CloudConfigurationTrustPolicy>();

private static CloudConfiguration CreateCustomCloud(string resourceManagerEndpoint, string activeDirectoryAuthority)
{
var element = JsonElementFactory.CreateElement(new Cloud
{
CurrentProfileName = "Custom",
Profiles = new Dictionary<string, CloudProfile>
{
["Custom"] = new(resourceManagerEndpoint, activeDirectoryAuthority),
}.ToImmutableSortedDictionary(),
CredentialPrecedence = [CredentialType.AzureCLI],
});

return CloudConfiguration.Bind(element);
}
}
Loading
Loading