From 57d0ae50164cc016ef4032f062ce676f53f4c645 Mon Sep 17 00:00:00 2001 From: Matthew Christopher Date: Fri, 27 Oct 2017 11:33:22 -0700 Subject: [PATCH] Add common changes to support Azure Batch AAD --- .../AzureEnvironment.cs | 12 +++++++++- .../AzureEnvironmentConstants.cs | 5 ++++ .../Extensions/AzureEnvironmentExtensions.cs | 13 ++++++++++ .../Interfaces/IAzureEnvironment.cs | 5 ++++ .../Models/PSAzureEnvironment.cs | 9 ++++++- src/ResourceManager/Profile/ChangeLog.md | 2 ++ .../AzureRMProfileTests.cs | 2 ++ .../EnvironmentCmdletTests.cs | 5 +++- .../TypeConversionTests.cs | 24 ++++++++++++------- .../Environment/AddAzureRMEnvironment.cs | 7 ++++++ .../Environment/SetAzureRMEnvironment.cs | 7 ++++++ 11 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironment.cs b/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironment.cs index 876c387c122c..df48320f2d6b 100644 --- a/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironment.cs +++ b/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironment.cs @@ -53,6 +53,7 @@ public class AzureEnvironment : IAzureEnvironment AzureDataLakeStoreFileSystemEndpointSuffix = AzureEnvironmentConstants.AzureDataLakeStoreFileSystemEndpointSuffix, GraphEndpointResourceId = AzureEnvironmentConstants.AzureGraphEndpoint, DataLakeEndpointResourceId = AzureEnvironmentConstants.AzureDataLakeServiceEndpointResourceId, + BatchEndpointResourceId = AzureEnvironmentConstants.BatchEndpointResourceId, AdTenant = "Common" } }, @@ -77,6 +78,7 @@ public class AzureEnvironment : IAzureEnvironment AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix = null, AzureDataLakeStoreFileSystemEndpointSuffix = null, GraphEndpointResourceId = AzureEnvironmentConstants.ChinaGraphEndpoint, + BatchEndpointResourceId = AzureEnvironmentConstants.ChinaBatchEndpointResourceId, AdTenant = "Common" } }, @@ -101,6 +103,7 @@ public class AzureEnvironment : IAzureEnvironment AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix = null, AzureDataLakeStoreFileSystemEndpointSuffix = null, GraphEndpointResourceId = AzureEnvironmentConstants.USGovernmentGraphEndpoint, + BatchEndpointResourceId = AzureEnvironmentConstants.USGovernmentBatchEndpointResourceId, AdTenant = "Common" } }, @@ -125,6 +128,7 @@ public class AzureEnvironment : IAzureEnvironment AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix = null, AzureDataLakeStoreFileSystemEndpointSuffix = null, GraphEndpointResourceId = AzureEnvironmentConstants.GermanGraphEndpoint, + BatchEndpointResourceId = AzureEnvironmentConstants.GermanBatchEndpointResourceId, AdTenant = "Common" } } @@ -228,6 +232,11 @@ public AzureEnvironment(IAzureEnvironment other) /// public string DataLakeEndpointResourceId { get; set; } + /// + /// The token audience required for communicating with the Batch service in this enviornment + /// + public string BatchEndpointResourceId { get; set; } + /// /// The domain name suffix for Azure DataLake Catalog and Job services created in this environment /// @@ -275,7 +284,8 @@ public static class Endpoint ManagementPortalUrl = "ManagementPortalUrl", AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix = "AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix", AzureDataLakeStoreFileSystemEndpointSuffix = "AzureDataLakeStoreFileSystemEndpointSuffix", - DataLakeEndpointResourceId = "DataLakeEndpointResourceId"; + DataLakeEndpointResourceId = "DataLakeEndpointResourceId", + BatchEndpointResourceId = "BatchEndpointResourceId"; } } diff --git a/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironmentConstants.cs b/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironmentConstants.cs index 090c86e40af1..647c98be3497 100644 --- a/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironmentConstants.cs +++ b/src/Common/Commands.Common.Authentication.Abstractions/AzureEnvironmentConstants.cs @@ -125,5 +125,10 @@ public static class AzureEnvironmentConstants /// The token audience for authorizing DataLake requests /// public const string AzureDataLakeServiceEndpointResourceId = "https://datalake.azure.net"; + + public const string BatchEndpointResourceId = "https://batch.core.windows.net/"; + public const string ChinaBatchEndpointResourceId = "https://batch.chinacloudapi.cn/"; + public const string USGovernmentBatchEndpointResourceId = "https://batch.core.usgovcloudapi.net/"; + public const string GermanBatchEndpointResourceId = "https://batch.cloudapi.de/"; } } diff --git a/src/Common/Commands.Common.Authentication.Abstractions/Extensions/AzureEnvironmentExtensions.cs b/src/Common/Commands.Common.Authentication.Abstractions/Extensions/AzureEnvironmentExtensions.cs index 7c836b6e5226..f886b754df89 100644 --- a/src/Common/Commands.Common.Authentication.Abstractions/Extensions/AzureEnvironmentExtensions.cs +++ b/src/Common/Commands.Common.Authentication.Abstractions/Extensions/AzureEnvironmentExtensions.cs @@ -65,6 +65,9 @@ public static bool TryGetEndpointUrl(this IAzureEnvironment environment, string case AzureEnvironment.Endpoint.DataLakeEndpointResourceId: endpoint = new Uri(environment.DataLakeEndpointResourceId); break; + case AzureEnvironment.Endpoint.BatchEndpointResourceId: + endpoint = new Uri(environment.BatchEndpointResourceId); + break; default: result = false; break; @@ -138,6 +141,9 @@ public static bool TryGetEndpointString(this IAzureEnvironment environment, stri case AzureEnvironment.Endpoint.ServiceManagement: propertyValue = environment.ServiceManagementUrl; break; + case AzureEnvironment.Endpoint.BatchEndpointResourceId: + propertyValue = environment.BatchEndpointResourceId; + break; default: break; } @@ -226,6 +232,9 @@ public static void SetEndpoint(this IAzureEnvironment environment, string endpoi case AzureEnvironment.Endpoint.DataLakeEndpointResourceId: environment.DataLakeEndpointResourceId = propertyValue; break; + case AzureEnvironment.Endpoint.BatchEndpointResourceId: + environment.BatchEndpointResourceId = propertyValue; + break; case AzureEnvironment.Endpoint.ActiveDirectory: environment.ActiveDirectoryAuthority = propertyValue; break; @@ -440,6 +449,10 @@ public static void CopyFrom(this IAzureEnvironment environment, IAzureEnvironmen environment.AzureKeyVaultServiceEndpointResourceId = other.AzureKeyVaultServiceEndpointResourceId; } + if (other.IsEndpointSet(AzureEnvironment.Endpoint.BatchEndpointResourceId)) + { + environment.BatchEndpointResourceId = other.BatchEndpointResourceId; + } environment.VersionProfiles.Clear(); foreach (var profile in other.VersionProfiles) diff --git a/src/Common/Commands.Common.Authentication.Abstractions/Interfaces/IAzureEnvironment.cs b/src/Common/Commands.Common.Authentication.Abstractions/Interfaces/IAzureEnvironment.cs index 76ee8bc71ac6..14f3f3371bc3 100644 --- a/src/Common/Commands.Common.Authentication.Abstractions/Interfaces/IAzureEnvironment.cs +++ b/src/Common/Commands.Common.Authentication.Abstractions/Interfaces/IAzureEnvironment.cs @@ -107,6 +107,11 @@ public interface IAzureEnvironment : IExtensibleModel /// string DataLakeEndpointResourceId { get; set; } + /// + /// The token audience required to authenticate with the Azure Batch service + /// + string BatchEndpointResourceId { get; set; } + /// /// The domain name suffix for Azure DataLake Catalog and Job services /// diff --git a/src/ResourceManager/Common/Commands.Common.Authentication.ResourceManager/Models/PSAzureEnvironment.cs b/src/ResourceManager/Common/Commands.Common.Authentication.ResourceManager/Models/PSAzureEnvironment.cs index ea81ad283993..4da8248c0d9d 100644 --- a/src/ResourceManager/Common/Commands.Common.Authentication.ResourceManager/Models/PSAzureEnvironment.cs +++ b/src/ResourceManager/Common/Commands.Common.Authentication.ResourceManager/Models/PSAzureEnvironment.cs @@ -95,6 +95,7 @@ public PSAzureEnvironment(PSObject other) AzureDataLakeStoreFileSystemEndpointSuffix = other.GetProperty(nameof(AzureDataLakeStoreFileSystemEndpointSuffix)); AzureKeyVaultDnsSuffix = other.GetProperty(nameof(AzureKeyVaultDnsSuffix)); AzureKeyVaultServiceEndpointResourceId = other.GetProperty(nameof(AzureKeyVaultServiceEndpointResourceId)); + BatchEndpointResourceId = other.GetProperty(nameof(BatchEndpointResourceId)); DataLakeEndpointResourceId = other.GetProperty(nameof(DataLakeEndpointResourceId)); GalleryUrl = other.GetProperty(nameof(GalleryUrl)); GraphEndpointResourceId = other.GetProperty(nameof(GraphEndpointResourceId)); @@ -227,6 +228,11 @@ public bool OnPremise public IDictionary ExtendedProperties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + /// Gets or sets the Azure Batch AD resource ID. + /// + public string BatchEndpointResourceId { get; set; } + /// /// Determine equality of two PSAzureEnvironment instances. /// @@ -253,7 +259,8 @@ public override bool Equals(object obj) && SqlDatabaseDnsSuffix == other.SqlDatabaseDnsSuffix && AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix == other.AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix && AzureDataLakeStoreFileSystemEndpointSuffix == other.AzureDataLakeStoreFileSystemEndpointSuffix - && TrafficManagerDnsSuffix == other.TrafficManagerDnsSuffix; + && TrafficManagerDnsSuffix == other.TrafficManagerDnsSuffix + && BatchEndpointResourceId == other.BatchEndpointResourceId; } return false; diff --git a/src/ResourceManager/Profile/ChangeLog.md b/src/ResourceManager/Profile/ChangeLog.md index a8b73ccc490d..6bb4e0dc4b21 100644 --- a/src/ResourceManager/Profile/ChangeLog.md +++ b/src/ResourceManager/Profile/ChangeLog.md @@ -28,6 +28,8 @@ - Clear-AzureRmDefault - Use this cmdlet to remove the current default resource group - ```Clear-AzureRmDefault -ResourceGroup``` +- Add-AzureRmEnvironment and Set-AzureRmEnvironment + - Add the BatchAudience parameter, which allows you to specify the Azure Batch Active Directory audience to use when acquiring authentication tokens for the Batch service. ## Version 3.4.1 * LocationCompleterAttribute added and available for cmdlets which use the -Location parameter diff --git a/src/ResourceManager/Profile/Commands.Profile.Test/AzureRMProfileTests.cs b/src/ResourceManager/Profile/Commands.Profile.Test/AzureRMProfileTests.cs index e6d7c4297193..8a90018e4126 100644 --- a/src/ResourceManager/Profile/Commands.Profile.Test/AzureRMProfileTests.cs +++ b/src/ResourceManager/Profile/Commands.Profile.Test/AzureRMProfileTests.cs @@ -733,6 +733,7 @@ public void SavingProfileWorks() ""AzureKeyVaultServiceEndpointResourceId"": null, ""GraphEndpointResourceId"": null, ""DataLakeEndpointResourceId"": null, + ""BatchEndpointResourceId"": null, ""AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix"": null, ""AzureDataLakeStoreFileSystemEndpointSuffix"": null, ""AdTenant"": null, @@ -784,6 +785,7 @@ public void SavingProfileWorks() ""AzureKeyVaultServiceEndpointResourceId"": null, ""GraphEndpointResourceId"": null, ""DataLakeEndpointResourceId"": null, + ""BatchEndpointResourceId"": null, ""AzureDataLakeAnalyticsCatalogAndJobEndpointSuffix"": null, ""AzureDataLakeStoreFileSystemEndpointSuffix"": null, ""AdTenant"": null, diff --git a/src/ResourceManager/Profile/Commands.Profile.Test/EnvironmentCmdletTests.cs b/src/ResourceManager/Profile/Commands.Profile.Test/EnvironmentCmdletTests.cs index b9fcb4bec596..3d8bca90a7b3 100644 --- a/src/ResourceManager/Profile/Commands.Profile.Test/EnvironmentCmdletTests.cs +++ b/src/ResourceManager/Profile/Commands.Profile.Test/EnvironmentCmdletTests.cs @@ -370,7 +370,8 @@ public void CanCreateEnvironmentWithAllProperties() StorageEndpoint = "https://StorageEndpoint", SqlDatabaseDnsSuffix = "SqlDatabaseDnsSuffix", TrafficManagerDnsSuffix = "TrafficManagerDnsSuffix", - GraphAudience = "GaraphAudience" + GraphAudience = "GaraphAudience", + BatchEndpointResourceId = "BatchResourceId" }; var dict = new Dictionary(); @@ -390,6 +391,7 @@ public void CanCreateEnvironmentWithAllProperties() dict["SqlDatabaseDnsSuffix"] = "SqlDatabaseDnsSuffix"; dict["TrafficManagerDnsSuffix"] = "TrafficManagerDnsSuffix"; dict["GraphAudience"] = "GaraphAudience"; + dict["BatchEndpointResourceId"] = "BatchResourceId"; cmdlet.SetBoundParameters(dict); cmdlet.InvokeBeginProcessing(); @@ -413,6 +415,7 @@ public void CanCreateEnvironmentWithAllProperties() Assert.Equal(cmdlet.SqlDatabaseDnsSuffix, actual.SqlDatabaseDnsSuffix); Assert.Equal(cmdlet.TrafficManagerDnsSuffix, actual.TrafficManagerDnsSuffix); Assert.Equal(cmdlet.GraphAudience, actual.GraphEndpointResourceId); + Assert.Equal(cmdlet.BatchEndpointResourceId, actual.BatchEndpointResourceId); commandRuntimeMock.Verify(f => f.WriteObject(It.IsAny()), Times.Once()); IAzureEnvironment env = AzureRmProfileProvider.Instance.Profile.GetEnvironment("KaTaL"); Assert.Equal(env.Name, cmdlet.Name); diff --git a/src/ResourceManager/Profile/Commands.Profile.Test/TypeConversionTests.cs b/src/ResourceManager/Profile/Commands.Profile.Test/TypeConversionTests.cs index 2e4093625b8a..5458796cfb04 100644 --- a/src/ResourceManager/Profile/Commands.Profile.Test/TypeConversionTests.cs +++ b/src/ResourceManager/Profile/Commands.Profile.Test/TypeConversionTests.cs @@ -55,6 +55,7 @@ public void CanConvertNullEnvironments() Assert.Null(environment.SqlDatabaseDnsSuffix); Assert.Null(environment.StorageEndpointSuffix); Assert.Null(environment.TrafficManagerDnsSuffix); + Assert.Null(environment.BatchEndpointResourceId); } [Theory] @@ -64,19 +65,19 @@ public void CanConvertNullEnvironments() "https://graph.windows.net", "https://graph.windows.net/", "https://manage.windowsazure.com", "https://manage.windowsazure.com/publishsettings", "https://management.azure.com", "https://management.core.windows.net", ".sql.azure.com", ".core.windows.net", - ".trafficmanager.windows.net")] + ".trafficmanager.windows.net", "https://batch.core.windows.net")] [Trait(Category.AcceptanceType, Category.CheckIn)] public void CanConvertValidEnvironments(string name, bool onPremise, string activeDirectory, string serviceResource, string adTenant, string dataLakeJobs, string dataLakeFiles, string kvDnsSuffix, string kvResource, string gallery, string graph, string graphResource, string portal, string publishSettings, string resourceManager, string serviceManagement, - string sqlSuffix, string storageSuffix, string trafficManagerSuffix) + string sqlSuffix, string storageSuffix, string trafficManagerSuffix, string batchResource) { AzureEnvironment azEnvironment = CreateEnvironment(name, onPremise, activeDirectory, serviceResource, adTenant, dataLakeJobs, dataLakeFiles, kvDnsSuffix, kvResource, gallery, graph, graphResource, portal, publishSettings, resourceManager, serviceManagement, sqlSuffix, storageSuffix, - trafficManagerSuffix); + trafficManagerSuffix, batchResource); var environment = (PSAzureEnvironment)azEnvironment; Assert.NotNull(environment); CheckEndpoint(AzureEnvironment.Endpoint.ActiveDirectory, azEnvironment, @@ -113,6 +114,8 @@ public void CanConvertValidEnvironments(string name, bool onPremise, string acti environment.StorageEndpointSuffix); CheckEndpoint(AzureEnvironment.Endpoint.TrafficManagerDnsSuffix, azEnvironment, environment.TrafficManagerDnsSuffix); + CheckEndpoint(AzureEnvironment.Endpoint.BatchEndpointResourceId, azEnvironment, + environment.BatchEndpointResourceId); Assert.Equal(azEnvironment.Name, environment.Name); Assert.Equal(azEnvironment.OnPremise, environment.EnableAdfsAuthentication); } @@ -144,6 +147,7 @@ public void CanConvertNullPSEnvironments() Assert.False(environment.IsEndpointSet(AzureEnvironment.Endpoint.SqlDatabaseDnsSuffix)); Assert.False(environment.IsEndpointSet(AzureEnvironment.Endpoint.StorageEndpointSuffix)); Assert.False(environment.IsEndpointSet(AzureEnvironment.Endpoint.TrafficManagerDnsSuffix)); + Assert.False(environment.IsEndpointSet(AzureEnvironment.Endpoint.BatchEndpointResourceId)); } [Theory] [InlineData("TestAll", true, "https://login.microsoftonline.com", "https://management.core.windows.net/", @@ -152,13 +156,13 @@ public void CanConvertNullPSEnvironments() "https://graph.windows.net", "https://graph.windows.net/", "https://manage.windowsazure.com", "https://manage.windowsazure.com/publishsettings", "https://management.azure.com", "https://management.core.windows.net", ".sql.azure.com", ".core.windows.net", - ".trafficmanager.windows.net")] + ".trafficmanager.windows.net", "https://batch.core.windows.net")] [Trait(Category.AcceptanceType, Category.CheckIn)] public void CanConvertValidPSEnvironments(string name, bool onPremise, string activeDirectory, string serviceResource, string adTenant, string dataLakeJobs, string dataLakeFiles, string kvDnsSuffix, string kvResource, string gallery, string graph, string graphResource, string portal, string publishSettings, string resourceManager, string serviceManagement, - string sqlSuffix, string storageSuffix, string trafficManagerSuffix) + string sqlSuffix, string storageSuffix, string trafficManagerSuffix, string batchResource) { PSAzureEnvironment environment = new PSAzureEnvironment { @@ -180,7 +184,8 @@ public void CanConvertValidPSEnvironments(string name, bool onPremise, string ac ServiceManagementUrl = serviceManagement, SqlDatabaseDnsSuffix = sqlSuffix, StorageEndpointSuffix = storageSuffix, - TrafficManagerDnsSuffix = trafficManagerSuffix + TrafficManagerDnsSuffix = trafficManagerSuffix, + BatchEndpointResourceId = batchResource }; var azEnvironment = (AzureEnvironment)environment; Assert.NotNull(environment); @@ -218,6 +223,8 @@ public void CanConvertValidPSEnvironments(string name, bool onPremise, string ac environment.StorageEndpointSuffix); CheckEndpoint(AzureEnvironment.Endpoint.TrafficManagerDnsSuffix, azEnvironment, environment.TrafficManagerDnsSuffix); + CheckEndpoint(AzureEnvironment.Endpoint.BatchEndpointResourceId, azEnvironment, + environment.BatchEndpointResourceId); Assert.Equal(azEnvironment.Name, environment.Name); Assert.Equal(azEnvironment.OnPremise, environment.EnableAdfsAuthentication); } @@ -227,7 +234,7 @@ private AzureEnvironment CreateEnvironment(string name, bool onPremise, string a string adTenant, string dataLakeJobs, string dataLakeFiles, string kvDnsSuffix, string kvResource, string gallery, string graph, string graphResource, string portal, string publishSettings, string resourceManager, string serviceManagement, - string sqlSuffix, string storageSuffix, string trafficManagerSuffix) + string sqlSuffix, string storageSuffix, string trafficManagerSuffix, string batchResource) { var environment = new AzureEnvironment() { Name = name, OnPremise = onPremise }; SetEndpoint(AzureEnvironment.Endpoint.ActiveDirectory, environment, activeDirectory); @@ -263,7 +270,8 @@ private AzureEnvironment CreateEnvironment(string name, bool onPremise, string a storageSuffix); CheckEndpoint(AzureEnvironment.Endpoint.TrafficManagerDnsSuffix, environment, trafficManagerSuffix); - + CheckEndpoint(AzureEnvironment.Endpoint.BatchEndpointResourceId, environment, + batchResource); return environment; } diff --git a/src/ResourceManager/Profile/Commands.Profile/Environment/AddAzureRMEnvironment.cs b/src/ResourceManager/Profile/Commands.Profile/Environment/AddAzureRMEnvironment.cs index 0c043a6188c6..baac85a35213 100644 --- a/src/ResourceManager/Profile/Commands.Profile/Environment/AddAzureRMEnvironment.cs +++ b/src/ResourceManager/Profile/Commands.Profile/Environment/AddAzureRMEnvironment.cs @@ -146,6 +146,11 @@ public string DataLakeAudience } } + [Parameter(Position = 20, Mandatory = false, ValueFromPipelineByPropertyName = true, + HelpMessage = "The resource identifier of the Azure Batch service that is the recipient of the requested token.")] + [Alias("BatchResourceId", "BatchAudience")] + public string BatchEndpointResourceId { get; set; } + protected override void BeginProcessing() { // do not call begin processing there is no context needed for this cmdlet @@ -285,6 +290,8 @@ public override void ExecuteCmdlet() nameof(GraphAudience)); SetEndpointIfBound(newEnvironment, AzureEnvironment.Endpoint.DataLakeEndpointResourceId, nameof(DataLakeAudience)); + SetEndpointIfBound(newEnvironment, AzureEnvironment.Endpoint.BatchEndpointResourceId, + nameof(BatchEndpointResourceId)); WriteObject(new PSAzureEnvironment(profileClient.AddOrSetEnvironment(newEnvironment))); } }); diff --git a/src/ResourceManager/Profile/Commands.Profile/Environment/SetAzureRMEnvironment.cs b/src/ResourceManager/Profile/Commands.Profile/Environment/SetAzureRMEnvironment.cs index b9363ff650b5..dd49da89368c 100644 --- a/src/ResourceManager/Profile/Commands.Profile/Environment/SetAzureRMEnvironment.cs +++ b/src/ResourceManager/Profile/Commands.Profile/Environment/SetAzureRMEnvironment.cs @@ -147,6 +147,11 @@ public string DataLakeAudience } } + [Parameter(Position = 20, Mandatory = false, ValueFromPipelineByPropertyName = true, + HelpMessage = "The resource identifier of the Azure Batch service that is the recipient of the requested token.")] + [Alias("BatchResourceId", "BatchAudience")] + public string BatchEndpointResourceId { get; set; } + protected override void BeginProcessing() { // do not call begin processing there is no context needed for this cmdlet @@ -286,6 +291,8 @@ public override void ExecuteCmdlet() nameof(GraphAudience)); SetEndpointIfBound(newEnvironment, AzureEnvironment.Endpoint.DataLakeEndpointResourceId, nameof(DataLakeAudience)); + SetEndpointIfBound(newEnvironment, AzureEnvironment.Endpoint.BatchEndpointResourceId, + nameof(BatchEndpointResourceId)); WriteObject(new PSAzureEnvironment(profileClient.AddOrSetEnvironment(newEnvironment))); } });