Skip to content

Commit 985f430

Browse files
committed
chore(git): Merge origin/main into certauth-kvsecretversion-57240
2 parents 8a2bc3e + 7facb58 commit 985f430

File tree

11 files changed

+106
-55
lines changed

11 files changed

+106
-55
lines changed

AzureAppGatewayOrchestrator.Tests/Client.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,12 @@ public void AzureClientIntegrationTest(string testAuthMethod)
9393
// Step 3 - Get the certificates that exist on the app gateway
9494

9595
// Act
96-
IEnumerable<Keyfactor.Orchestrators.Extensions.CurrentInventoryItem> certs = client.GetAppGatewaySslCertificates();
96+
OperationResult<IEnumerable<Keyfactor.Orchestrators.Extensions.CurrentInventoryItem>> certs = client.GetAppGatewaySslCertificates();
9797

9898
// Assert
99-
Assert.NotNull(certs);
100-
Assert.NotEmpty(certs);
101-
Assert.Contains(certs, c => c.Alias == certName);
99+
Assert.NotNull(certs.Result);
100+
Assert.NotEmpty(certs.Result);
101+
Assert.Contains(certs.Result, c => c.Alias == certName);
102102

103103
// Step 4 - Try to remove the certificate from the app gateway, which should fail
104104
// since it's bound to an HTTPS listener
@@ -125,7 +125,7 @@ public void AzureClientIntegrationTest(string testAuthMethod)
125125
// Rebind the HTTPS listener with the original certificate, if there was only 1 certificate
126126
// previously, otherwise bind it with the first certificate in the list.
127127

128-
ApplicationGatewaySslCertificate replacement = client.GetAppGatewayCertificateByName(certs.First(c => c.Alias != certName).Alias);
128+
ApplicationGatewaySslCertificate replacement = client.GetAppGatewayCertificateByName(certs.Result.First(c => c.Alias != certName).Alias);
129129

130130
client.UpdateHttpsListenerCertificate(replacement, env.HttpsListenerName);
131131

AzureAppGatewayOrchestrator.Tests/FakeClient.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public IAzureAppGatewayClient Build()
8282
public IEnumerable<string>? AppGatewaysAvailableOnFakeTenant { get; set; }
8383
public Dictionary<string, ApplicationGatewaySslCertificate>? CertificatesAvailableOnFakeAppGateway { get; set; }
8484

85-
public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
85+
public OperationResult<IEnumerable<CurrentInventoryItem>> GetAppGatewaySslCertificates()
8686
{
8787
_logger.LogDebug("Getting App Gateway SSL Certificates from fake app gateway");
8888

@@ -92,6 +92,8 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
9292
}
9393

9494
List<CurrentInventoryItem> inventoryItems = new List<CurrentInventoryItem>();
95+
OperationResult<IEnumerable<CurrentInventoryItem>> result = new(inventoryItems);
96+
9597
foreach (ApplicationGatewaySslCertificate cert in CertificatesAvailableOnFakeAppGateway.Values)
9698
{
9799
inventoryItems.Add(new CurrentInventoryItem
@@ -106,7 +108,7 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
106108

107109
_logger.LogDebug($"Fake client has {inventoryItems.Count} certificates in inventory");
108110

109-
return inventoryItems;
111+
return result;
110112
}
111113

112114
public ApplicationGatewaySslCertificate AddCertificate(string certificateName, string certificateData, string certificatePassword)

AzureAppGatewayOrchestrator/AppGatewayCertificateJobs/Inventory.cs

+24-2
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,32 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
5151

5252
try
5353
{
54-
inventoryItems = Client.GetAppGatewaySslCertificates().ToList();
54+
OperationResult<IEnumerable<CurrentInventoryItem>> inventoryResult = Client.GetAppGatewaySslCertificates();
55+
if (!inventoryResult.Success)
56+
{
57+
// Aggregate the messages into the failure message. Since an exception wasn't thrown,
58+
// we still have a partial success. We want to return a warning.
59+
result.FailureMessage += inventoryResult.ErrorMessage;
60+
result.Result = OrchestratorJobStatusJobResult.Warning;
61+
_logger.LogWarning(result.FailureMessage);
62+
}
63+
else
64+
{
65+
result.Result = OrchestratorJobStatusJobResult.Success;
66+
}
67+
68+
// At least partial success is guaranteed, so we can continue with the inventory items
69+
// that we were able to pull down.
70+
inventoryItems = inventoryResult.Result.ToList();
71+
5572
} catch (Exception ex)
5673
{
74+
// Exception is triggered if we weren't able to pull down the list of certificates
75+
// from Azure. This could be due to a number of reasons, including network issues,
76+
// or the user not having the correct permissions. An exception won't be triggered
77+
// if there are no certificates in the App Gateway, or if we weren't able to assemble
78+
// the list of certificates into a CurrentInventoryItem.
79+
5780
_logger.LogError(ex, "Error getting App Gateway SSL Certificates:\n" + ex.Message);
5881
result.FailureMessage = "Error getting App Gateway SSL Certificates:\n" + ex.Message;
5982
return result;
@@ -64,7 +87,6 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
6487
//cb.DynamicInvoke(inventoryItems);
6588
cb(inventoryItems);
6689

67-
result.Result = OrchestratorJobStatusJobResult.Success;
6890
return result;
6991
}
7092
}

AzureAppGatewayOrchestrator/Client/GatewayClient.cs

+16-5
Original file line numberDiff line numberDiff line change
@@ -311,15 +311,16 @@ public ApplicationGatewaySslCertificate GetAppGatewayCertificateByName(string ce
311311
return gatewaySslCertificate;
312312
}
313313

314-
public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
314+
public OperationResult<IEnumerable<CurrentInventoryItem>> GetAppGatewaySslCertificates()
315315
{
316-
317316
ApplicationGatewayResource appGatewayResource =
318317
_armClient.GetApplicationGatewayResource(AppGatewayResourceId).Get();
319318
_logger.LogDebug($"Getting SSL certificates from App Gateway called \"{appGatewayResource.Data.Name}\"");
320319
_logger.LogDebug($"There are {appGatewayResource.Data.SslCertificates.Count()} certificates in the response.");
321320
List<CurrentInventoryItem> inventoryItems = new List<CurrentInventoryItem>();
322321

322+
OperationResult<IEnumerable<CurrentInventoryItem>> result = new(inventoryItems);
323+
323324
foreach (ApplicationGatewaySslCertificate certObject in appGatewayResource.Data.SslCertificates)
324325
{
325326
List<string> b64EncodedDerCertificateList = new List<string>();
@@ -354,14 +355,19 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
354355
}
355356
catch (Exception e)
356357
{
357-
_logger.LogError($"Error retrieving certificate from Azure Key Vault with ID {certObject.KeyVaultSecretId}: {e.Message}");
358+
string error = $"Failed to download certificate from Azure Key Vault with ID {certObject.KeyVaultSecretId}";
359+
_logger.LogError(error + $": {e.Message}");
360+
361+
result.AddRuntimeErrorMessage(error);
358362
continue;
359363
}
360364
}
361365
else
362366
{
363-
_logger.LogError($"Certificate called \"{certObject.Name}\" ({certObject.Id}) does not have any public certificate data or Key Vault secret ID.");
367+
string error = $"Certificate called \"{certObject.Name}\" ({certObject.Id}) does not have any public certificate data or Key Vault secret ID.";
368+
_logger.LogError(error);
364369

370+
result.AddRuntimeErrorMessage(error);
365371
continue;
366372
}
367373

@@ -378,8 +384,13 @@ public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates()
378384
inventoryItems.Add(inventoryItem);
379385
}
380386

387+
if (!result.Success)
388+
{
389+
result.ErrorSummary = $"Application Gateway Certificate inventory may be incomplete. Successfully read {inventoryItems.Count()}/{appGatewayResource.Data.SslCertificates.Count()} certificates present in the Application Gateway called {AppGatewayResourceId.Name} ({AppGatewayResourceId})\nPlease see Orchestrator logs for more details. Error summary:";
390+
}
391+
381392
_logger.LogDebug($"Found {inventoryItems.Count()} certificates in app gateway");
382-
return inventoryItems;
393+
return result;
383394
}
384395

385396
public void UpdateHttpsListenerCertificate(ApplicationGatewaySslCertificate certificate, string listenerName)

AzureAppGatewayOrchestrator/Client/IAzureAppGatewayClient.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,31 @@ public interface IAzureAppGatewayClientBuilder
3030
public IAzureAppGatewayClient Build();
3131
}
3232

33+
public class OperationResult<T>
34+
{
35+
public T Result { get; set; }
36+
public string ErrorSummary { get; set; }
37+
public List<string> Messages { get; set; } = new List<string>();
38+
public bool Success => Messages.Count == 0;
39+
40+
public OperationResult(T result)
41+
{
42+
Result = result;
43+
}
44+
45+
public void AddRuntimeErrorMessage(string message)
46+
{
47+
Messages.Add(" - " + message);
48+
}
49+
50+
public string ErrorMessage => $"{ErrorSummary}\n{string.Join("\n", Messages)}";
51+
}
52+
3353
public interface IAzureAppGatewayClient
3454
{
3555
public ApplicationGatewaySslCertificate AddCertificate(string certificateName, string certificateData, string certificatePassword);
3656
public void RemoveCertificate(string certificateName);
37-
public IEnumerable<CurrentInventoryItem> GetAppGatewaySslCertificates();
57+
public OperationResult<IEnumerable<CurrentInventoryItem>> GetAppGatewaySslCertificates();
3858
public ApplicationGatewaySslCertificate GetAppGatewayCertificateByName(string certificateName);
3959
public bool CertificateExists(string certificateName);
4060
public IEnumerable<string> DiscoverApplicationGateways();

AzureAppGatewayOrchestrator/ListenerBindingJobs/Inventory.cs

+28-4
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
4444
JobResult result = new JobResult
4545
{
4646
Result = OrchestratorJobStatusJobResult.Failure,
47-
JobHistoryId = config.JobHistoryId
47+
JobHistoryId = config.JobHistoryId,
48+
FailureMessage = ""
4849
};
4950

5051
List<CurrentInventoryItem> appGatewayCertificateInventory;
@@ -62,9 +63,32 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
6263

6364
try
6465
{
65-
appGatewayCertificateInventory = Client.GetAppGatewaySslCertificates().ToList();
66+
OperationResult<IEnumerable<CurrentInventoryItem>> inventoryResult = Client.GetAppGatewaySslCertificates();
67+
if (!inventoryResult.Success)
68+
{
69+
// Aggregate the messages into the failure message. Since an exception wasn't thrown,
70+
// we still have a partial success. We want to return a warning.
71+
result.FailureMessage += inventoryResult.ErrorMessage;
72+
result.Result = OrchestratorJobStatusJobResult.Warning;
73+
_logger.LogWarning(result.FailureMessage);
74+
}
75+
else
76+
{
77+
result.Result = OrchestratorJobStatusJobResult.Success;
78+
}
79+
80+
// At least partial success is guaranteed, so we can continue with the inventory items
81+
// that we were able to pull down.
82+
appGatewayCertificateInventory = inventoryResult.Result.ToList();
83+
6684
} catch (Exception ex)
6785
{
86+
// Exception is triggered if we weren't able to pull down the list of certificates
87+
// from Azure. This could be due to a number of reasons, including network issues,
88+
// or the user not having the correct permissions. An exception won't be triggered
89+
// if there are no certificates in the App Gateway, or if we weren't able to assemble
90+
// the list of certificates into a CurrentInventoryItem.
91+
6892
_logger.LogError(ex, "Error getting App Gateway SSL Certificates:\n" + ex.Message);
6993
result.FailureMessage = "Error getting App Gateway SSL Certificates:\n" + ex.Message;
7094
return result;
@@ -81,7 +105,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
81105
} catch (Exception ex)
82106
{
83107
_logger.LogError(ex, "Error getting bound App Gateway HTTPS Listener Certificates:\n" + ex.Message);
84-
result.FailureMessage = "Error getting bound App Gateway HTTPS Listener Certificates:\n" + ex.Message;
108+
result.FailureMessage += "Error getting bound App Gateway HTTPS Listener Certificates:\n" + ex.Message;
85109
return result;
86110
}
87111

@@ -129,7 +153,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
129153

130154
cb.DynamicInvoke(certificateBindingInventory);
131155

132-
result.Result = OrchestratorJobStatusJobResult.Success;
156+
// Result is already set correctly by this point.
133157
return result;
134158
}
135159
}

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@
1717
- chore(jobs): Refactored Orchestrator job implementations to prefer dependency injection pattern.
1818
- chore(tests): Implemented unit testing framework with a fake client interface.
1919
- chore(tests): Implemented integration tests for both Orchestrator jobs and App Gateway client.
20+
21+
- 2.1.0
22+
- chore(client): Pass error back to Command if certificate download from AKV fails

Makefile

+1-28
Original file line numberDiff line numberDiff line change
@@ -62,33 +62,6 @@ setup: ## Setup the environment for development
6262
echo "PROJECT_NAME=$$(basename $$(dirname $$(grep PROJECT_FILE .env | cut -d '=' -f 2)))" >> .env; \
6363
fi
6464

65-
.PHONY: newtest
66-
newtest: setup ## Create a new test project
67-
@source .env; \
68-
testProjectName="$$PROJECT_NAME".Tests; \
69-
echo "Creating new xUnit project called $$testProjectName"; \
70-
dotnet new xunit -o $$testProjectName; \
71-
dotnet sln add $$testProjectName/$$testProjectName.csproj; \
72-
dotnet add $$testProjectName reference $$PROJECT_FILE;
73-
74-
.PHONY: installpackage
75-
installpackage: ## Install a package to the project
76-
@source .env; \
77-
echo "Select a project to install the package into"; \
78-
PS3="Selection: "; \
79-
select opt in $$(ls */*.csproj); do \
80-
if [ -n "$$opt" ]; then \
81-
echo "You have selected $$opt"; \
82-
break; \
83-
else \
84-
echo "Invalid selection. Please try again."; \
85-
fi; \
86-
done; \
87-
echo "Enter the package name to install: "; \
88-
read packageName; \
89-
echo "Installing $$packageName to $$opt"; \
90-
dotnet add $$opt package $$packageName;
91-
9265
.PHONY: testall
9366
testall: ## Run all tests.
9467
@source .env; \
@@ -105,7 +78,7 @@ test: ## Run a single test.
10578
cut -d ' ' -f 5- | \
10679
sed 's/(.*//i' | \
10780
sort | uniq | \
108-
fzf | \
81+
fzf | \
10982
xargs -I {} dotnet test --filter {} --logger "console;verbosity=detailed"
11083

11184
##@ Build

README.md

+2-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The Universal Orchestrator is the successor to the Windows Orchestrator. This Or
1515

1616
## Support for Azure Application Gateway Orchestrator
1717

18-
Azure Application Gateway Orchestrator is open source and supported on best effort level for this tool/library/client. This means customers can report Bugs, Feature Requests, Documentation amendment or questions as well as requests for customer information required for setup that needs Keyfactor access to obtain. Such requests do not follow normal SLA commitments for response or resolution. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com/
18+
Azure Application Gateway Orchestrator is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com
1919

2020
###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab.
2121

@@ -101,9 +101,7 @@ Natively, Azure Application Gateways support integration with Azure Key Vault fo
101101

102102
#### Mechanics of the Azure Key Vault Download Operation for Inventory Jobs that report certificates imported from AKV
103103

104-
If an AzureApplicationSslCertificate references a secret in AKV (was imported to the App Gateway from AKV), the inventory job will create and use a `SecretClient` from the [`Azure.Security.KeyVault.Secrets.SecretClient` dotnet package](https://learn.microsoft.com/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient?view=azure-dotnet). Authentication to AKV via this client is configured using the exact same `TokenCredential` provided by the [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet). This means that the Service Principal described in the [Azure Configuration](#azure-configuration) section must also have appropriate permissions to read secrets from the AKV that the App Gateway is integrated with.
105-
106-
The secret referenced in the AzureApplicationSslCertificate will be accessed exactly as reported by Azure, regardless of whether it exists in AKV. Since the App Gateway orchestrator extension doesn't manage AKV secrets in any way, the client will _only log an error_ if the client is unsuccessful in downloading the secret for any reason. IE, if the request to AKV fails after five tries, the imported certificate will not be reported to Keyfactor Command in Inventory operations. This design choice was made based on the logical existance or non-existance of the link between AKV and the App Gateway.
104+
If an AzureApplicationSslCertificate references a secret in AKV (was imported to the App Gateway from AKV), the inventory job will create and use a `SecretClient` from the [`Azure.Security.KeyVault.Secrets.SecretClient` dotnet package](https://learn.microsoft.com/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient?view=azure-dotnet). Authentication to AKV via this client is configured using the exact same `TokenCredential` provided by the [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet). This means that the Service Principal described in the [Azure Configuration](#azure-configuration) section must also have appropriate permissions to read secrets from the AKV that the App Gateway is integrated with. The secret referenced in the AzureApplicationSslCertificate will be accessed exactly as reported by Azure, regardless of whether it exists in AKV.
107105

108106
## Azure Configuration and Permissions
109107

integration-manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"integration_type": "orchestrator",
55
"status": "production",
66
"description": "The Azure Application Gateway Orchestrator Extension is an extension to the Keyfactor Universal Orchestrator that allows for the management of certificates on Azure Application Gateways, including the ability to add and bind certificates to HTTPS listeners.",
7-
"support_level": "kf-community",
7+
"support_level": "kf-supported",
88
"release_dir": "AzureAppGatewayOrchestrator\\bin\\Release",
99
"link_github": true,
1010
"update_catalog": true,

0 commit comments

Comments
 (0)