diff --git a/.doc_gen/metadata/controltower_metadata.yaml b/.doc_gen/metadata/controltower_metadata.yaml index 3cf49427046..083079b4db1 100644 --- a/.doc_gen/metadata/controltower_metadata.yaml +++ b/.doc_gen/metadata/controltower_metadata.yaml @@ -12,6 +12,14 @@ controltower_Hello: - description: snippet_tags: - python.example_code.controltower.Hello + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.HelloControlTower services: controltower: {ListBaselines} @@ -26,6 +34,14 @@ controltower_ListBaselines: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.ListBaselines + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.ListBaselines services: controltower: {ListBaselines} @@ -40,6 +56,14 @@ controltower_ListEnabledBaselines: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.ListEnabledBaselines + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.ListEnabledBaselines services: controltower: {ListEnabledBaselines} @@ -54,6 +78,14 @@ controltower_EnableBaseline: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.EnableBaseline + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.EnableBaseline services: controltower: {EnableBaseline} @@ -68,6 +100,14 @@ controltower_ResetEnabledBaseline: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.ResetEnabledBaseline + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.ResetEnabledBaseline services: controltower: {ResetEnabledBaseline} @@ -82,6 +122,14 @@ controltower_DisableBaseline: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.DisableBaseline + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.DisableBaseline services: controltower: {DisableBaseline} @@ -96,6 +144,14 @@ controltower_ListEnabledControls: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.ListEnabledControls + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.ListEnabledControls services: controltower: {ListEnabledControls} @@ -110,6 +166,14 @@ controltower_EnableControl: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.EnableControl + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.EnableControl services: controltower: {EnableControl} @@ -124,6 +188,14 @@ controltower_GetControlOperation: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.GetControlOperation + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.GetControlOperation services: controltower: {GetControlOperation} @@ -138,6 +210,14 @@ controltower_DisableControl: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.DisableControl + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.DisableControl services: controltower: {DisableControl} @@ -152,9 +232,39 @@ controltower_ListLandingZones: snippet_tags: - python.example_code.controltower.ControlTowerWrapper.decl - python.example_code.controltower.ListLandingZones + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.ListLandingZones services: controltower: {ListLandingZones} +controltower_GetBaselineOperation: + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/controltower + excerpts: + - description: + snippet_tags: + - python.example_code.controltower.ControlTowerWrapper.decl + - python.example_code.controltower.GetBaselineOperation + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: + snippet_tags: + - ControlTower.dotnetv4.GetBaselineOperation + services: + controltower: {GetBaselineOperation} + controltower_Scenario: synopsis_list: - List landing zones. @@ -172,5 +282,16 @@ controltower_Scenario: snippet_tags: - python.example_code.controltower.ControlTowerScenario - python.example_code.controltower.ControlTowerWrapper.class + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/ControlTower + excerpts: + - description: Run an interactive scenario demonstrating &CTowerlong; features. + snippet_tags: + - ControlTower.dotnetv4.ControlTowerBasics + - description: Wrapper methods that are called by the scenario to manage &AUR; actions. + snippet_tags: + - ControlTower.dotnetv4.ControlTowerWrapper services: controltower: {CreateLandingZone, DeleteLandingZone, ListBaselines, ListEnabledBaselines, EnableBaseline, ResetEnabledBaseline, DisableBaseline, EnableControl, GetControlOperation, DisableControl, GetLandingZoneOperation, ListLandingZones, ListEnabledControls} diff --git a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj new file mode 100644 index 00000000000..d989156a2c3 --- /dev/null +++ b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + \ No newline at end of file diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs new file mode 100644 index 00000000000..baff9a65f54 --- /dev/null +++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs @@ -0,0 +1,511 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper] + +using Amazon.ControlCatalog; +using Amazon.ControlCatalog.Model; +using Amazon.ControlTower; +using Amazon.ControlTower.Model; +using ValidationException = Amazon.ControlTower.Model.ValidationException; + +namespace ControlTowerActions; + +/// +/// Methods to perform AWS Control Tower actions. +/// +public class ControlTowerWrapper +{ + private readonly IAmazonControlTower _controlTowerService; + private readonly IAmazonControlCatalog _controlCatalogService; + + /// + /// Constructor for the wrapper class containing AWS Control Tower actions. + /// + /// The AWS Control Tower client object. + /// The AWS Control Catalog client object. + public ControlTowerWrapper(IAmazonControlTower controlTowerService, IAmazonControlCatalog controlCatalogService) + { + _controlTowerService = controlTowerService; + _controlCatalogService = controlCatalogService; + } + + // snippet-start:[ControlTower.dotnetv4.ListLandingZones] + /// + /// List the AWS Control Tower landing zones for an account. + /// + /// A list of LandingZoneSummary objects. + public async Task> ListLandingZonesAsync() + { + try + { + var landingZones = new List(); + + var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest()); + + await foreach (var response in landingZonesPaginator.Responses) + { + landingZones.AddRange(response.LandingZones); + } + + return landingZones; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't list landing zones. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.ListLandingZones] + + // snippet-start:[ControlTower.dotnetv4.ListBaselines] + /// + /// List all baselines. + /// + /// A list of baseline summaries. + public async Task> ListBaselinesAsync() + { + try + { + var baselines = new List(); + + var baselinesPaginator = _controlTowerService.Paginators.ListBaselines(new ListBaselinesRequest()); + + await foreach (var response in baselinesPaginator.Responses) + { + baselines.AddRange(response.Baselines); + } + + return baselines; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't list baselines. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.ListBaselines] + + // snippet-start:[ControlTower.dotnetv4.ListEnabledBaselines] + /// + /// List all enabled baselines. + /// + /// A list of enabled baseline summaries. + public async Task> ListEnabledBaselinesAsync() + { + try + { + var enabledBaselines = new List(); + + var enabledBaselinesPaginator = _controlTowerService.Paginators.ListEnabledBaselines(new ListEnabledBaselinesRequest()); + + await foreach (var response in enabledBaselinesPaginator.Responses) + { + enabledBaselines.AddRange(response.EnabledBaselines); + } + + return enabledBaselines; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't list enabled baselines. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.ListEnabledBaselines] + + // snippet-start:[ControlTower.dotnetv4.EnableBaseline] + /// + /// Enable a baseline for the specified target. + /// + /// The ARN of the target. + /// The identifier of baseline to enable. + /// The version of baseline to enable. + /// The identifier of identity center baseline if it is enabled. + /// The enabled baseline ARN or null if already enabled. + public async Task EnableBaselineAsync(string targetIdentifier, string baselineIdentifier, string baselineVersion, string identityCenterBaseline) + { + try + { + var parameters = new List + { + new EnabledBaselineParameter + { + Key = "IdentityCenterEnabledBaselineArn", + Value = identityCenterBaseline + } + }; + + var request = new EnableBaselineRequest + { + BaselineIdentifier = baselineIdentifier, + BaselineVersion = baselineVersion, + TargetIdentifier = targetIdentifier, + Parameters = parameters + }; + + var response = await _controlTowerService.EnableBaselineAsync(request); + var operationId = response.OperationIdentifier; + + // Wait for operation to complete + while (true) + { + var status = await GetBaselineOperationAsync(operationId); + Console.WriteLine($"Baseline operation status: {status}"); + if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED) + { + break; + } + await Task.Delay(30000); // Wait 30 seconds + } + + return response.Arn; + } + catch (ValidationException ex) when (ex.Message.Contains("already enabled")) + { + Console.WriteLine("Baseline is already enabled for this target"); + return null; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't enable baseline. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.EnableBaseline] + + // snippet-start:[ControlTower.dotnetv4.DisableBaseline] + /// + /// Disable a baseline for a specific target and wait for the operation to complete. + /// + /// The identifier of the baseline to disable. + /// The operation ID or null if there was a conflict. + public async Task DisableBaselineAsync(string enabledBaselineIdentifier) + { + try + { + var request = new DisableBaselineRequest + { + EnabledBaselineIdentifier = enabledBaselineIdentifier + }; + + var response = await _controlTowerService.DisableBaselineAsync(request); + var operationId = response.OperationIdentifier; + + // Wait for operation to complete + while (true) + { + var status = await GetBaselineOperationAsync(operationId); + Console.WriteLine($"Baseline operation status: {status}"); + if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED) + { + break; + } + await Task.Delay(30000); // Wait 30 seconds + } + + return operationId; + } + catch (ConflictException ex) + { + Console.WriteLine($"Conflict disabling baseline: {ex.Message}. Skipping disable step."); + return null; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't disable baseline. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.DisableBaseline] + + // snippet-start:[ControlTower.dotnetv4.ResetEnabledBaseline] + /// + /// Reset an enabled baseline for a specific target. + /// + /// The identifier of the enabled baseline to reset. + /// The operation ID. + public async Task ResetEnabledBaselineAsync(string enabledBaselineIdentifier) + { + try + { + var request = new ResetEnabledBaselineRequest + { + EnabledBaselineIdentifier = enabledBaselineIdentifier + }; + + var response = await _controlTowerService.ResetEnabledBaselineAsync(request); + var operationId = response.OperationIdentifier; + + // Wait for operation to complete + while (true) + { + var status = await GetBaselineOperationAsync(operationId); + Console.WriteLine($"Baseline operation status: {status}"); + if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED) + { + break; + } + await Task.Delay(30000); // Wait 30 seconds + } + + return operationId; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException) + { + Console.WriteLine("Target not found, unable to reset enabled baseline."); + throw; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't reset enabled baseline. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.ResetEnabledBaseline] + + // snippet-start:[ControlTower.dotnetv4.GetBaselineOperation] + /// + /// Get the status of a baseline operation. + /// + /// The ID of the baseline operation. + /// The operation status. + public async Task GetBaselineOperationAsync(string operationId) + { + try + { + var request = new GetBaselineOperationRequest + { + OperationIdentifier = operationId + }; + + var response = await _controlTowerService.GetBaselineOperationAsync(request); + return response.BaselineOperation.Status; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException) + { + Console.WriteLine("Operation not found."); + throw; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't get baseline operation status. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.GetBaselineOperation] + + // snippet-start:[ControlTower.dotnetv4.ListEnabledControls] + /// + /// List enabled controls for a target organizational unit. + /// + /// The target organizational unit identifier. + /// A list of enabled control summaries. + public async Task> ListEnabledControlsAsync(string targetIdentifier) + { + try + { + var request = new ListEnabledControlsRequest + { + TargetIdentifier = targetIdentifier + }; + + var enabledControls = new List(); + + var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request); + + await foreach (var response in enabledControlsPaginator.Responses) + { + enabledControls.AddRange(response.EnabledControls); + } + + return enabledControls; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower")) + { + Console.WriteLine("AWS Control Tower must be enabled to work with enabling controls."); + return new List(); + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't list enabled controls. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.ListEnabledControls] + + // snippet-start:[ControlTower.dotnetv4.EnableControl] + /// + /// Enable a control for a specified target. + /// + /// The ARN of the control to enable. + /// The identifier of the target (e.g., OU ARN). + /// The operation ID or null if already enabled. + public async Task EnableControlAsync(string controlArn, string targetIdentifier) + { + try + { + Console.WriteLine(controlArn); + Console.WriteLine(targetIdentifier); + + var request = new EnableControlRequest + { + ControlIdentifier = controlArn, + TargetIdentifier = targetIdentifier + }; + + var response = await _controlTowerService.EnableControlAsync(request); + var operationId = response.OperationIdentifier; + + // Wait for operation to complete + while (true) + { + var status = await GetControlOperationAsync(operationId); + Console.WriteLine($"Control operation status: {status}"); + if (status == ControlOperationStatus.SUCCEEDED || status == ControlOperationStatus.FAILED) + { + break; + } + await Task.Delay(30000); // Wait 30 seconds + } + + return operationId; + } + catch (Amazon.ControlTower.Model.ValidationException ex) when (ex.Message.Contains("already enabled")) + { + Console.WriteLine("Control is already enabled for this target"); + return null; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower")) + { + Console.WriteLine("AWS Control Tower must be enabled to work with enabling controls."); + return null; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't enable control. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.EnableControl] + + // snippet-start:[ControlTower.dotnetv4.DisableControl] + /// + /// Disable a control for a specified target. + /// + /// The ARN of the control to disable. + /// The identifier of the target (e.g., OU ARN). + /// The operation ID. + public async Task DisableControlAsync(string controlArn, string targetIdentifier) + { + try + { + var request = new DisableControlRequest + { + ControlIdentifier = controlArn, + TargetIdentifier = targetIdentifier + }; + + var response = await _controlTowerService.DisableControlAsync(request); + var operationId = response.OperationIdentifier; + + // Wait for operation to complete + while (true) + { + var status = await GetControlOperationAsync(operationId); + Console.WriteLine($"Control operation status: {status}"); + if (status == ControlOperationStatus.SUCCEEDED || status == ControlOperationStatus.FAILED) + { + break; + } + await Task.Delay(30000); // Wait 30 seconds + } + + return operationId; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException) + { + Console.WriteLine("Control not found."); + throw; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't disable control. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.DisableControl] + + // snippet-start:[ControlTower.dotnetv4.GetControlOperation] + /// + /// Get the status of a control operation. + /// + /// The ID of the control operation. + /// The operation status. + public async Task GetControlOperationAsync(string operationId) + { + try + { + var request = new GetControlOperationRequest + { + OperationIdentifier = operationId + }; + + var response = await _controlTowerService.GetControlOperationAsync(request); + return response.ControlOperation.Status; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException) + { + Console.WriteLine("Operation not found."); + throw; + } + catch (AmazonControlTowerException ex) + { + Console.WriteLine($"Couldn't get control operation status. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.GetControlOperation] + + // snippet-start:[ControlTower.dotnetv4.ListControls] + /// + /// List all controls in the Control Tower control catalog. + /// + /// A list of control summaries. + public async Task> ListControlsAsync() + { + try + { + var controls = new List(); + + var controlsPaginator = _controlCatalogService.Paginators.ListControls(new Amazon.ControlCatalog.Model.ListControlsRequest()); + + await foreach (var response in controlsPaginator.Responses) + { + controls.AddRange(response.Controls); + } + + return controls; + } + catch (AmazonControlCatalogException ex) + { + Console.WriteLine($"Couldn't list controls. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; + } + } + + // snippet-end:[ControlTower.dotnetv4.ListControls] +} + +// snippet-end:[ControlTower.dotnetv4.ControlTowerWrapper] \ No newline at end of file diff --git a/dotnetv4/ControlTower/Actions/HelloControlTower.cs b/dotnetv4/ControlTower/Actions/HelloControlTower.cs new file mode 100644 index 00000000000..bab047bf390 --- /dev/null +++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.HelloControlTower] + +using Amazon.ControlTower; +using Amazon.ControlTower.Model; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Logging.Debug; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace ControlTowerActions; + +/// +/// A class that introduces the AWS Control Tower by listing the +/// available baselines for the account. +/// +public class HelloControlTower +{ + private static ILogger logger = null!; + + static async Task Main(string[] args) + { + // Set up dependency injection for AWS Control Tower. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + logging.AddFilter("System", LogLevel.Debug) + .AddFilter("Microsoft", LogLevel.Information) + .AddFilter("Microsoft", LogLevel.Trace)) + .ConfigureServices((_, services) => + services.AddAWSService() + ) + .Build(); + + logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) + .CreateLogger(); + + var amazonClient = host.Services.GetRequiredService(); + + Console.Clear(); + Console.WriteLine("Hello, AWS Control Tower! Let's list available baselines:"); + Console.WriteLine(); + + var baselines = new List(); + + try + { + var baselinesPaginator = amazonClient.Paginators.ListBaselines(new ListBaselinesRequest()); + + await foreach (var response in baselinesPaginator.Responses) + { + baselines.AddRange(response.Baselines); + } + + Console.WriteLine($"{baselines.Count} baseline(s) retrieved."); + foreach (var baseline in baselines) + { + Console.WriteLine($"\t{baseline.Name}"); + } + } + catch (Amazon.ControlTower.Model.AccessDeniedException) + { + Console.WriteLine("Access denied. Please ensure you have the necessary permissions."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + } +} + +// snippet-end:[ControlTower.dotnetv4.HelloControlTower] \ No newline at end of file diff --git a/dotnetv4/ControlTower/ControlTowerExamples.sln b/dotnetv4/ControlTower/ControlTowerExamples.sln new file mode 100644 index 00000000000..b064902934c --- /dev/null +++ b/dotnetv4/ControlTower/ControlTowerExamples.sln @@ -0,0 +1,47 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{7907FB6A-1353-4735-95DC-EEC5DF8C0649}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{B987097B-189C-4D0B-99BC-E67CD705BCA0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5455D423-2AFC-4BC6-B79D-9DC4270D8F7D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerActions", "Actions\ControlTowerActions.csproj", "{796910FA-6E94-460B-8CB4-97DF01B9ADC8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerBasics", "Scenarios\ControlTower_Basics\ControlTowerBasics.csproj", "{B1731AE1-381F-4044-BEBE-269FF7E24B1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerTests", "Tests\ControlTowerTests.csproj", "{6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.Build.0 = Release|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.Build.0 = Release|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {796910FA-6E94-460B-8CB4-97DF01B9ADC8} = {7907FB6A-1353-4735-95DC-EEC5DF8C0649} + {B1731AE1-381F-4044-BEBE-269FF7E24B1F} = {B987097B-189C-4D0B-99BC-E67CD705BCA0} + {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88} = {5455D423-2AFC-4BC6-B79D-9DC4270D8F7D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {870D888D-5C8B-4057-8722-F73ECF38E513} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md new file mode 100644 index 00000000000..226fdc22170 --- /dev/null +++ b/dotnetv4/ControlTower/README.md @@ -0,0 +1,119 @@ +# AWS Control Tower code examples for the SDK for .NET (v4) + +## Overview + +Shows how to use the AWS SDK for .NET (v4) to work with AWS Control Tower. + + + + +_AWS Control Tower enables you to enforce and manage governance rules for security, operations, and compliance at scale across all your organizations and accounts._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + + + + + +### Get started + +- [Hello AWS Control Tower](Actions/HelloControlTower.cs#L4) (`ListBaselines`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](Scenarios/ControlTower_Basics/ControlTowerBasics.cs) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [DisableBaseline](Actions/ControlTowerWrapper.cs#L181) +- [DisableControl](Actions/ControlTowerWrapper.cs#L401) +- [EnableBaseline](Actions/ControlTowerWrapper.cs#L120) +- [EnableControl](Actions/ControlTowerWrapper.cs#L345) +- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L273) +- [GetControlOperation](Actions/ControlTowerWrapper.cs#L449) +- [ListBaselines](Actions/ControlTowerWrapper.cs#L62) +- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L91) +- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L305) +- [ListLandingZones](Actions/ControlTowerWrapper.cs#L33) +- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L227) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS Control Tower + +This example shows you how to get started using AWS Control Tower. + + +#### Learn the basics + +This example shows you how to do the following: + +- List landing zones. +- List, enable, get, reset, and disable baselines. +- List, enable, get, and disable controls. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../README.md#Tests) +in the `dotnetv4` folder. + + + + + + +## Additional resources + +- [AWS Control Tower User Guide](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html) +- [AWS Control Tower API Reference](https://docs.aws.amazon.com/controltower/latest/APIReference/Welcome.html) +- [SDK for .NET (v4) AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/Controltower/NControltower.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs new file mode 100644 index 00000000000..9d5e3e212da --- /dev/null +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs @@ -0,0 +1,295 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.ControlTowerBasics] + +using Amazon.ControlCatalog; +using Amazon.ControlTower; +using Amazon.ControlTower.Model; +using Amazon.Organizations; +using Amazon.Organizations.Model; +using Amazon.SecurityToken; +using Amazon.SecurityToken.Model; +using ControlTowerActions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace ControlTowerBasics; + +/// +/// Scenario class for AWS Control Tower basics. +/// +public class ControlTowerBasics +{ + public static bool isInteractive = true; + public static ILogger logger = null!; + public static IAmazonOrganizations? orgClient = null; + public static IAmazonSecurityTokenService? stsClient = null; + public static ControlTowerWrapper? wrapper = null; + private static string? ouArn; + private static bool useLandingZone = false; + + /// + /// Main entry point for the AWS Control Tower basics scenario. + /// + /// Command line arguments. + public static async Task Main(string[] args) + { + using var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddAWSService() + .AddAWSService() + .AddAWSService() + .AddTransient() + ) + .Build(); + + logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) + .CreateLogger(); + + wrapper = host.Services.GetRequiredService(); + orgClient = host.Services.GetRequiredService(); + stsClient = host.Services.GetRequiredService(); + + await RunScenario(); + } + + /// + /// Runs the example scenario. + /// + public static async Task RunScenario() + { + Console.WriteLine(new string('-', 88)); + Console.WriteLine("\tWelcome to the AWS Control Tower with ControlCatalog example scenario."); + Console.WriteLine(new string('-', 88)); + Console.WriteLine("This demo will walk you through working with AWS Control Tower for landing zones,"); + Console.WriteLine("managing baselines, and working with controls."); + + try + { + var accountId = (await stsClient!.GetCallerIdentityAsync(new GetCallerIdentityRequest())).Account; + Console.WriteLine($"\nAccount ID: {accountId}"); + + Console.WriteLine("\nSome demo operations require the use of a landing zone."); + Console.WriteLine("You can use an existing landing zone or opt out of these operations in the demo."); + Console.WriteLine("For instructions on how to set up a landing zone,"); + Console.WriteLine("see https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html"); + + // List available landing zones + var landingZones = await wrapper!.ListLandingZonesAsync(); + if (landingZones.Count > 0) + { + Console.WriteLine("\nAvailable Landing Zones:"); + for (int i = 0; i < landingZones.Count; i++) + { + Console.WriteLine($"{i + 1}. {landingZones[i].Arn}"); + } + + Console.Write($"\nDo you want to use the first landing zone in the list ({landingZones[0].Arn})? (y/n): "); + if (GetUserConfirmation()) + { + useLandingZone = true; + Console.WriteLine($"Using landing zone: {landingZones[0].Arn}"); + ouArn = await SetupOrganizationAsync(); + } + } + + // Managing Baselines + Console.WriteLine("\nManaging Baselines:"); + var baselines = await wrapper.ListBaselinesAsync(); + Console.WriteLine("\nListing available Baselines:"); + BaselineSummary? controlTowerBaseline = null; + foreach (var baseline in baselines) + { + if (baseline.Name == "AWSControlTowerBaseline") + controlTowerBaseline = baseline; + Console.WriteLine($" - {baseline.Name}"); + } + + EnabledBaselineSummary? identityCenterBaseline = null; + string? baselineArn = null; + + if (useLandingZone && ouArn != null) + { + Console.WriteLine("\nListing enabled baselines:"); + var enabledBaselines = await wrapper.ListEnabledBaselinesAsync(); + foreach (var baseline in enabledBaselines) + { + if (baseline.BaselineIdentifier.Contains("baseline/LN25R72TTG6IGPTQ")) + identityCenterBaseline = baseline; + Console.WriteLine($" - {baseline.BaselineIdentifier}"); + } + + if (controlTowerBaseline != null) + { + Console.Write("\nDo you want to enable the Control Tower Baseline? (y/n): "); + if (GetUserConfirmation()) + { + Console.WriteLine("\nEnabling Control Tower Baseline."); + var icBaselineArn = identityCenterBaseline?.Arn; + baselineArn = await wrapper.EnableBaselineAsync(ouArn, + controlTowerBaseline.Arn, "4.0", icBaselineArn ?? ""); + var alreadyEnabled = false; + if (baselineArn != null) + { + Console.WriteLine($"Enabled baseline ARN: {baselineArn}"); + } + else + { + // Find the enabled baseline + foreach (var enabled in enabledBaselines) + { + if (enabled.BaselineIdentifier == controlTowerBaseline.Arn) + { + baselineArn = enabled.Arn; + break; + } + } + + alreadyEnabled = true; + Console.WriteLine("No change, the selected baseline was already enabled."); + } + + if (baselineArn != null) + { + Console.Write("\nDo you want to reset the Control Tower Baseline? (y/n): "); + if (GetUserConfirmation()) + { + Console.WriteLine($"\nResetting Control Tower Baseline: {baselineArn}"); + var operationId = await wrapper.ResetEnabledBaselineAsync(baselineArn); + Console.WriteLine($"Reset baseline operation id: {operationId}"); + } + + Console.Write("\nDo you want to disable the Control Tower Baseline? (y/n): "); + if (GetUserConfirmation()) + { + Console.WriteLine($"Disabling baseline ARN: {baselineArn}"); + var operationId = await wrapper.DisableBaselineAsync(baselineArn); + Console.WriteLine($"Disabled baseline operation id: {operationId}"); + if (alreadyEnabled) + { + Console.WriteLine($"\nRe-enabling Control Tower Baseline: {baselineArn}"); + // Re-enable the Control Tower baseline if it was originally enabled. + await wrapper.EnableBaselineAsync(ouArn, + controlTowerBaseline.Arn, "4.0", icBaselineArn ?? ""); + } + } + } + } + } + } + + // Managing Controls + Console.WriteLine("\nManaging Controls:"); + var controls = await wrapper.ListControlsAsync(); + Console.WriteLine("\nListing first 5 available Controls:"); + for (int i = 0; i < Math.Min(5, controls.Count); i++) + { + Console.WriteLine($"{i + 1}. {controls[i].Name} - {controls[i].Arn}"); + } + + if (useLandingZone && ouArn != null) + { + var enabledControls = await wrapper.ListEnabledControlsAsync(ouArn); + Console.WriteLine("\nListing enabled controls:"); + for (int i = 0; i < enabledControls.Count; i++) + { + Console.WriteLine($"{i + 1}. {enabledControls[i].ControlIdentifier}"); + } + + // Find first non-enabled control + var enabledControlArns = enabledControls.Select(c => c.Arn).ToHashSet(); + var controlArn = controls.FirstOrDefault(c => !enabledControlArns.Contains(c.Arn))?.Arn; + + if (controlArn != null) + { + Console.Write($"\nDo you want to enable the control {controlArn}? (y/n): "); + if (GetUserConfirmation()) + { + Console.WriteLine($"\nEnabling control: {controlArn}"); + var operationId = await wrapper.EnableControlAsync(controlArn, ouArn); + if (operationId != null) + { + Console.WriteLine($"Enabled control with operation id: {operationId}"); + + Console.Write("\nDo you want to disable the control? (y/n): "); + if (GetUserConfirmation()) + { + Console.WriteLine("\nDisabling the control..."); + var disableOpId = await wrapper.DisableControlAsync(controlArn, ouArn); + Console.WriteLine($"Disable operation ID: {disableOpId}"); + } + } + } + } + } + + Console.WriteLine("\nThis concludes the example scenario."); + Console.WriteLine("Thanks for watching!"); + Console.WriteLine(new string('-', 88)); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred during the Control Tower scenario."); + Console.WriteLine($"An error occurred: {ex.Message}"); + } + } + + /// + /// Sets up AWS Organizations and creates or finds a Sandbox OU. + /// + /// The ARN of the Sandbox organizational unit. + private static async Task SetupOrganizationAsync() + { + Console.WriteLine("\nChecking organization status..."); + + try + { + var orgResponse = await orgClient!.DescribeOrganizationAsync(new DescribeOrganizationRequest()); + var orgId = orgResponse.Organization.Id; + Console.WriteLine($"Account is part of organization: {orgId}"); + } + catch (AWSOrganizationsNotInUseException) + { + Console.WriteLine("No organization found. Creating a new organization..."); + var createResponse = await orgClient!.CreateOrganizationAsync(new CreateOrganizationRequest { FeatureSet = OrganizationFeatureSet.ALL }); + var orgId = createResponse.Organization.Id; + Console.WriteLine($"Created new organization: {orgId}"); + } + + // Look for Sandbox OU + var roots = await orgClient.ListRootsAsync(new ListRootsRequest()); + var rootId = roots.Roots[0].Id; + + Console.WriteLine("Checking for Sandbox OU..."); + var ous = await orgClient.ListOrganizationalUnitsForParentAsync(new ListOrganizationalUnitsForParentRequest { ParentId = rootId }); + var sandboxOu = ous.OrganizationalUnits.FirstOrDefault(ou => ou.Name == "Sandbox"); + + if (sandboxOu == null) + { + Console.WriteLine("Creating Sandbox OU..."); + var createOuResponse = await orgClient.CreateOrganizationalUnitAsync(new CreateOrganizationalUnitRequest { ParentId = rootId, Name = "Sandbox" }); + sandboxOu = createOuResponse.OrganizationalUnit; + Console.WriteLine($"Created new Sandbox OU: {sandboxOu.Id}"); + } + else + { + Console.WriteLine($"Found existing Sandbox OU: {sandboxOu.Id}"); + } + + return sandboxOu.Arn; + } + + /// + /// Gets user confirmation by waiting for input or returning true if not interactive. + /// + /// True if user enters 'y' or if isInteractive is false, otherwise false. + private static bool GetUserConfirmation() + { + return Console.ReadLine()?.ToLower() == "y" || !isInteractive; + } +} + +// snippet-end:[ControlTower.dotnetv4.ControlTowerBasics] \ No newline at end of file diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj new file mode 100644 index 00000000000..78d5b798214 --- /dev/null +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + PreserveNewest + settings.json + + + + \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs new file mode 100644 index 00000000000..3aac260da5b --- /dev/null +++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs @@ -0,0 +1,276 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.ControlCatalog; +using Amazon.ControlCatalog.Model; +using Amazon.ControlTower; +using Amazon.ControlTower.Model; +using Amazon.Organizations; +using Amazon.Organizations.Model; +using Amazon.Runtime; +using Amazon.SecurityToken; +using Amazon.SecurityToken.Model; +using ControlTowerActions; +using Microsoft.Extensions.Logging; +using Moq; + +namespace ControlTowerTests; + +/// +/// Integration tests for the AWS Control Tower Basics scenario. +/// +public class ControlTowerBasicsTests +{ + /// + /// Verifies the scenario with an integration test. No errors should be logged. + /// + /// Async task. + [Fact] + [Trait("Category", "Integration")] + public async Task TestScenarioIntegration() + { + // Arrange + ControlTowerBasics.ControlTowerBasics.isInteractive = false; + var loggerScenarioMock = new Mock>(); + + loggerScenarioMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); + + // Act + ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object; + + ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(new AmazonControlTowerClient(), new AmazonControlCatalogClient()); + ControlTowerBasics.ControlTowerBasics.orgClient = new AmazonOrganizationsClient(); + ControlTowerBasics.ControlTowerBasics.stsClient = new AmazonSecurityTokenServiceClient(); + + + await ControlTowerBasics.ControlTowerBasics.RunScenario(); + + // Assert no errors logged + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + + /// + /// Scenario test using mocked AWS service clients. + /// + /// Async task. + [Fact] + [Trait("Category", "Unit")] + public async Task TestScenarioLogic() + { + // Arrange + var mockControlTower = new Mock(); + var mockControlCatalog = new Mock(); + var mockOrganizations = new Mock(); + var mockSts = new Mock(); + + SetupMocks(mockControlTower, mockControlCatalog, mockOrganizations, mockSts); + + ControlTowerBasics.ControlTowerBasics.isInteractive = false; + ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object); + ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object; + ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object; + + var loggerScenarioMock = new Mock>(); + ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object; + + // Act + await ControlTowerBasics.ControlTowerBasics.RunScenario(); + + // Assert no errors logged + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + + /// + /// Test scenario with ControlTowerException. + /// + /// Async task. + [Fact] + [Trait("Category", "Unit")] + public async Task TestScenarioWithException() + { + // Arrange + var mockControlTower = new Mock(); + var mockControlCatalog = new Mock(); + var mockOrganizations = new Mock(); + var mockSts = new Mock(); + + SetupMocks(mockControlTower, mockControlCatalog, mockOrganizations, mockSts, throwException: true); + + ControlTowerBasics.ControlTowerBasics.isInteractive = false; + ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object); + ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object; + ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object; + + var loggerScenarioMock = new Mock>(); + ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object; + + // Act + await ControlTowerBasics.ControlTowerBasics.RunScenario(); + + // Assert the error is logged. + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + /// + /// Set up the mocks for testing. + /// + /// Mock ControlTower client. + /// Mock ControlCatalog client. + /// Mock Organizations client. + /// Mock Sts client. + /// True to force an exception. + private void SetupMocks(Mock mockControlTower, Mock mockControlCatalog, Mock mockOrganizations, Mock mockSts, bool throwException = false) + { + // Setup paginator mocks + var mockLandingZonesPaginator = new Mock(); + var mockLandingZonesEnumerable = new Mock>(); + + mockLandingZonesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None)) + .Returns(new List + { + new ListLandingZonesResponse { LandingZones = + new List + { + new LandingZoneSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:landingzone/test-lz" } + } } + }.ToAsyncEnumerable().GetAsyncEnumerator()); + + + mockLandingZonesPaginator.Setup(x => x.Responses).Returns(mockLandingZonesEnumerable.Object); + mockControlTower.Setup(x => x.Paginators.ListLandingZones(It.IsAny())) + .Returns(mockLandingZonesPaginator.Object); + + var mockBaselinesPaginator = new Mock(); + var mockBaselinesEnumerable = new Mock>(); + mockBaselinesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None)) + .Returns(new List + { + new ListBaselinesResponse { Baselines = + new List + { + new BaselineSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:baseline/test-baseline", Name = "AWSControlTowerBaseline" } + } } + }.ToAsyncEnumerable().GetAsyncEnumerator()); + mockBaselinesPaginator.Setup(x => x.Responses).Returns(mockBaselinesEnumerable.Object); + mockControlTower.Setup(x => x.Paginators.ListBaselines(It.IsAny())) + .Returns(mockBaselinesPaginator.Object); + + var mockEnabledBaselinesPaginator = new Mock(); + var mockEnabledBaselinesEnumerable = new Mock>(); + mockEnabledBaselinesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None)) + .Returns(new List + { + new ListEnabledBaselinesResponse { EnabledBaselines = + new List + { + new EnabledBaselineSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-enabled", BaselineIdentifier = "baseline/LN25R72TTG6IGPTQ" } + } } + }.ToAsyncEnumerable().GetAsyncEnumerator()); + mockEnabledBaselinesPaginator.Setup(x => x.Responses).Returns(mockEnabledBaselinesEnumerable.Object); + mockControlTower.Setup(x => x.Paginators.ListEnabledBaselines(It.IsAny())) + .Returns(mockEnabledBaselinesPaginator.Object); + + var mockEnabledControlsPaginator = new Mock(); + var mockEnabledControlsEnumerable = new Mock>(); + mockEnabledControlsEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None)) + .Returns(new List + { + new ListEnabledControlsResponse { EnabledControls = + new List + { + new EnabledControlSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:control/test-control", ControlIdentifier = "arn:aws:controltower:us-east-1:123456789012:control/test-control-identifier" } + } } + }.ToAsyncEnumerable().GetAsyncEnumerator()); + mockEnabledControlsPaginator.Setup(x => x.Responses).Returns(mockEnabledControlsEnumerable.Object); + mockControlTower.Setup(x => x.Paginators.ListEnabledControls(It.IsAny())) + .Returns(mockEnabledControlsPaginator.Object); + + var mockControlsPaginator = new Mock(); + var mockControlsEnumerable = new Mock>(); + mockControlsEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None)) + .Returns(new List + { + new Amazon.ControlCatalog.Model.ListControlsResponse { Controls = + new List + { + new Amazon.ControlCatalog.Model.ControlSummary { Arn = "arn:aws:controlcatalog:us-east-1::control/ABCDEFG1234567", Name = "Test Control" } + } } + }.ToAsyncEnumerable().GetAsyncEnumerator()); + mockControlsPaginator.Setup(x => x.Responses).Returns(mockControlsEnumerable.Object); + mockControlCatalog.Setup(x => x.Paginators.ListControls(It.IsAny())) + .Returns(mockControlsPaginator.Object); + + // Force an exception that should end the scenario. + if (throwException) + { + mockControlTower.Setup(x => + x.DisableBaselineAsync(It.IsAny(), default)) + .Throws(new AmazonControlTowerException("Test exception")); + } + else + { + mockControlTower.Setup(x => x.DisableBaselineAsync(It.IsAny(), default)) + .ReturnsAsync(new DisableBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" }); + } + mockControlTower.Setup(x => + x.EnableBaselineAsync(It.IsAny(), default)) + .ReturnsAsync(new EnableBaselineResponse + { + Arn = + "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-baseline", + OperationIdentifier = "12345678-1234-1234-1234-123456789012" + }); + + mockControlTower.Setup(x => x.ResetEnabledBaselineAsync(It.IsAny(), default)) + .ReturnsAsync(new ResetEnabledBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" }); + mockControlTower.Setup(x => x.GetBaselineOperationAsync(It.IsAny(), default)) + .ReturnsAsync(new GetBaselineOperationResponse { BaselineOperation = new BaselineOperation { Status = BaselineOperationStatus.SUCCEEDED } }); + mockControlTower.Setup(x => x.EnableControlAsync(It.IsAny(), default)) + .ReturnsAsync(new EnableControlResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" }); + mockControlTower.Setup(x => x.DisableControlAsync(It.IsAny(), default)) + .ReturnsAsync(new DisableControlResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" }); + mockControlTower.Setup(x => x.GetControlOperationAsync(It.IsAny(), default)) + .ReturnsAsync(new GetControlOperationResponse { ControlOperation = new ControlOperation { Status = ControlOperationStatus.SUCCEEDED } }); + mockControlTower.Setup(x => x.GetLandingZoneAsync(It.IsAny(), default)) + .ReturnsAsync(new GetLandingZoneResponse { LandingZone = new LandingZoneDetail() }); + + // Setup Organizations mocks + mockOrganizations.Setup(x => x.DescribeOrganizationAsync(It.IsAny(), default)) + .ReturnsAsync(new DescribeOrganizationResponse { Organization = new Organization { Id = "o-test123456" } }); + mockOrganizations.Setup(x => x.ListRootsAsync(It.IsAny(), default)) + .ReturnsAsync(new ListRootsResponse { Roots = new List { new Root { Id = "r-test123", Arn = "arn:aws:organizations::123456789012:root/o-test123456/r-test123" } } }); + mockOrganizations.Setup(x => x.ListOrganizationalUnitsForParentAsync(It.IsAny(), default)) + .ReturnsAsync(new ListOrganizationalUnitsForParentResponse { OrganizationalUnits = new List { new OrganizationalUnit { Id = "ou-test1234-abcd5678", Name = "Sandbox", Arn = "arn:aws:organizations::123456789012:ou/o-test123456/ou-test1234-abcd5678" } } }); + + // Setup STS mocks + mockSts.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), default)) + .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" }); + } +} \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj new file mode 100644 index 00000000000..44b2474f876 --- /dev/null +++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + PreserveNewest + testsettings.json + + + + + + + + + \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/Usings.cs b/dotnetv4/ControlTower/Tests/Usings.cs new file mode 100644 index 00000000000..0f64a5599c7 --- /dev/null +++ b/dotnetv4/ControlTower/Tests/Usings.cs @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using Xunit; + +// Optional. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln index 55584997279..57acbb70c60 100644 --- a/dotnetv4/DotNetV4Examples.sln +++ b/dotnetv4/DotNetV4Examples.sln @@ -137,6 +137,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECSScenario", "ECS\ECSScena EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECSActions", "ECS\ECSActions\ECSActions.csproj", "{7485EAED-F81C-4119-BABC-E009A21ACE46}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ControlTower", "ControlTower", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerTests", "ControlTower\Tests\ControlTowerTests.csproj", "{43C5E98B-5EC4-9F2B-2676-8F1E34969855}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{6BE1D9A4-1832-49F5-8682-6DEE4A7D6232}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerBasics", "ControlTower\Scenarios\ControlTower_Basics\ControlTowerBasics.csproj", "{6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerActions", "ControlTower\Actions\ControlTowerActions.csproj", "{9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -335,6 +345,18 @@ Global {7485EAED-F81C-4119-BABC-E009A21ACE46}.Debug|Any CPU.Build.0 = Debug|Any CPU {7485EAED-F81C-4119-BABC-E009A21ACE46}.Release|Any CPU.ActiveCfg = Release|Any CPU {7485EAED-F81C-4119-BABC-E009A21ACE46}.Release|Any CPU.Build.0 = Release|Any CPU + {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Release|Any CPU.Build.0 = Release|Any CPU + {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Release|Any CPU.Build.0 = Release|Any CPU + {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -398,6 +420,10 @@ Global {3F159C49-3DE7-42F5-AF14-E64C03AF19E8} = {EE6D1933-1E38-406A-B691-446326310D1F} {D44D50E1-EC65-4A1C-AAA1-C360E4FC563F} = {EE6D1933-1E38-406A-B691-446326310D1F} {7485EAED-F81C-4119-BABC-E009A21ACE46} = {EE6D1933-1E38-406A-B691-446326310D1F} + {43C5E98B-5EC4-9F2B-2676-8F1E34969855} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6} = {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} + {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} diff --git a/python/example_code/controltower/README.md b/python/example_code/controltower/README.md index 596edf11dee..726705ff9ef 100644 --- a/python/example_code/controltower/README.md +++ b/python/example_code/controltower/README.md @@ -56,6 +56,7 @@ Code excerpts that show you how to call individual service functions. - [DisableControl](controltower_wrapper.py#L263) - [EnableBaseline](controltower_wrapper.py#L69) - [EnableControl](controltower_wrapper.py#L159) +- [GetBaselineOperation](controltower_wrapper.py#L236) - [GetControlOperation](controltower_wrapper.py#L209) - [ListBaselines](controltower_wrapper.py#L39) - [ListEnabledBaselines](controltower_wrapper.py#L330)