From 45646f39f334f302fc6f95b8ae9d819981b20106 Mon Sep 17 00:00:00 2001
From: Hayden Roszell
Date: Tue, 24 Sep 2024 16:07:38 -0700
Subject: [PATCH 01/10] chore(oid): Split App/SP client & jobs to use OID
instead of App ID
Signed-off-by: Hayden Roszell
---
.../AzureApp.cs | 40 +--
.../AzureSP.cs | 40 +--
.../Client.cs | 62 +++-
.../FakeClient.cs | 20 +-
.../IntegrationTestingFact.cs | 16 +-
.../.DS_Store | Bin 6148 -> 0 bytes
.../AzureAppJobs/Discovery.cs | 10 +-
.../AzureSPJobs/Discovery.cs | 10 +-
.../Client/GraphClient.cs | 269 +++++++-----------
.../Client/IAzureGraphClient.cs | 5 +-
.../GraphJobClientBuilder.cs | 19 +-
11 files changed, 245 insertions(+), 246 deletions(-)
delete mode 100644 AzureEnterpriseApplicationOrchestrator/.DS_Store
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs b/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs
index 56d7432..68615c2 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs
@@ -25,7 +25,7 @@ namespace AzureEnterpriseApplicationOrchestrator.Tests;
public class AzureEnterpriseApplicationOrchestrator_AzureApp
{
- ILogger _logger { get; set;}
+ ILogger _logger { get; set; }
public AzureEnterpriseApplicationOrchestrator_AzureApp()
{
@@ -48,7 +48,7 @@ public void AzureApp_Inventory_IntegrationTest_ReturnSuccess()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
- .WithTargetApplicationId(env.TargetApplicationId)
+ .WithTargetObjectId(env.TargetApplicationObjectId)
.Build();
// Set up the inventory job configuration
@@ -57,8 +57,8 @@ public void AzureApp_Inventory_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetApplicationId,
- Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ StorePath = env.TargetApplicationObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
}
};
@@ -110,21 +110,21 @@ public void AzureApp_Inventory_ProcessJob_ValidClient_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = "test",
- StorePath = "test",
- Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
},
- JobHistoryId = 1
+ JobHistoryId = 1
};
// Act
JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
{
- // Assert
- Assert.Equal(1, inventoryItems.Count());
- Assert.Equal("test", inventoryItems.First().Alias);
+ // Assert
+ Assert.Equal(1, inventoryItems.Count());
+ Assert.Equal("test", inventoryItems.First().Alias);
- _logger.LogInformation("AzureApp_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
- return true;
+ _logger.LogInformation("AzureApp_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
});
// Assert
@@ -149,10 +149,10 @@ public void AzureApp_Inventory_ProcessJob_InvalidClient_ReturnFailure()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = "test",
- StorePath = "test",
- Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
},
- JobHistoryId = 1
+ JobHistoryId = 1
};
bool callbackCalled = false;
@@ -215,7 +215,7 @@ public void AzureApp_Discovery_ProcessJob_ValidClient_ReturnSuccess()
// Arrange
IAzureGraphClient client = new FakeClient
{
- ApplicationIdsAvailableOnFakeTenant = new List { "test" }
+ ObjectIdsAvailableOnFakeTenant = new List { "test" }
};
// Set up the discovery job with the fake client
@@ -481,8 +481,8 @@ public void AzureApp_Management_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetApplicationId,
- Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ StorePath = env.TargetApplicationObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
},
JobCertificate = new ManagementJobCertificate
{
@@ -505,7 +505,7 @@ public void AzureApp_Management_IntegrationTest_ReturnSuccess()
ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(testHostname);
b64Cert = Convert.ToBase64String(ssCert.Export(X509ContentType.Cert));
-
+
config.OperationType = CertStoreOperationType.Add;
config.Overwrite = true;
config.JobCertificate = new ManagementJobCertificate
@@ -554,7 +554,7 @@ static void ConfigureLogging()
LogHandler.Factory = LoggerFactory.Create(builder =>
{
- builder.AddNLog();
+ builder.AddNLog();
});
}
}
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs b/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs
index 704df42..c78f740 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs
@@ -25,7 +25,7 @@ namespace AzureEnterpriseApplicationOrchestrator.Tests;
public class AzureEnterpriseApplicationOrchestrator_AzureSP
{
- ILogger _logger { get; set;}
+ ILogger _logger { get; set; }
public AzureEnterpriseApplicationOrchestrator_AzureSP()
{
@@ -49,7 +49,7 @@ public void AzureSP_Inventory_IntegrationTest_ReturnSuccess()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
- .WithTargetApplicationId(env.TargetApplicationId)
+ .WithTargetObjectId(env.TargetServicePrincipalObjectId)
.Build();
// Set up the inventory job configuration
@@ -58,8 +58,8 @@ public void AzureSP_Inventory_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetApplicationId,
- Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ StorePath = env.TargetServicePrincipalObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
}
};
@@ -111,21 +111,21 @@ public void AzureSP_Inventory_ProcessJob_ValidClient_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = "test",
- StorePath = "test",
- Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
},
- JobHistoryId = 1
+ JobHistoryId = 1
};
// Act
JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
{
- // Assert
- Assert.Equal(1, inventoryItems.Count());
- Assert.Equal("test", inventoryItems.First().Alias);
+ // Assert
+ Assert.Equal(1, inventoryItems.Count());
+ Assert.Equal("test", inventoryItems.First().Alias);
- _logger.LogInformation("AzureSP_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
- return true;
+ _logger.LogInformation("AzureSP_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
});
// Assert
@@ -150,10 +150,10 @@ public void AzureSP_Inventory_ProcessJob_InvalidClient_ReturnFailure()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = "test",
- StorePath = "test",
- Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
},
- JobHistoryId = 1
+ JobHistoryId = 1
};
bool callbackCalled = false;
@@ -216,7 +216,7 @@ public void AzureSP_Discovery_ProcessJob_ValidClient_ReturnSuccess()
// Arrange
IAzureGraphClient client = new FakeClient
{
- ApplicationIdsAvailableOnFakeTenant = new List { "test" }
+ ObjectIdsAvailableOnFakeTenant = new List { "test" }
};
// Set up the discovery job with the fake client
@@ -486,8 +486,8 @@ public void AzureSP_Management_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetApplicationId,
- Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ StorePath = env.TargetServicePrincipalObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
},
JobCertificate = new ManagementJobCertificate
{
@@ -511,7 +511,7 @@ public void AzureSP_Management_IntegrationTest_ReturnSuccess()
ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(testHostname);
b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
-
+
config.OperationType = CertStoreOperationType.Add;
config.Overwrite = true;
config.JobCertificate = new ManagementJobCertificate
@@ -561,7 +561,7 @@ static void ConfigureLogging()
LogHandler.Factory = LoggerFactory.Create(builder =>
{
- builder.AddNLog();
+ builder.AddNLog();
});
}
}
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/Client.cs b/AzureEnterpriseApplicationOrchestrator.Tests/Client.cs
index 18db671..8387bf6 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/Client.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/Client.cs
@@ -42,7 +42,7 @@ public void GraphClient_Application_AddGetRemove_ReturnSuccess(string testAuthMe
IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
- .WithTargetApplicationId(env.TargetApplicationId);
+ .WithTargetObjectId(env.TargetApplicationObjectId);
if (testAuthMethod == "clientcert")
{
@@ -53,7 +53,7 @@ public void GraphClient_Application_AddGetRemove_ReturnSuccess(string testAuthMe
var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
clientBuilder.WithClientCertificate(cert);
}
-
+
IAzureGraphClient client = clientBuilder.Build();
// Step 1 - Add the certificate to the Application
@@ -68,13 +68,13 @@ public void GraphClient_Application_AddGetRemove_ReturnSuccess(string testAuthMe
// Act
OperationResult> operationResult = client.GetApplicationCertificates();
-
+
// Assert
Assert.True(operationResult.Success);
Assert.NotNull(operationResult.Result);
Assert.True(operationResult.Result.Any(c => c.Alias == certName));
Assert.True(operationResult.Result.Any(c => c.Alias == certName && c.PrivateKeyEntry == false));
-
+
// Step 3 - Determine if the certificate exists in the Application
// Act
@@ -115,7 +115,7 @@ public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess(string testA
IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
- .WithTargetApplicationId(env.TargetApplicationId);
+ .WithTargetObjectId(env.TargetServicePrincipalObjectId);
if (testAuthMethod == "clientcert")
{
@@ -126,7 +126,7 @@ public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess(string testA
var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
clientBuilder.WithClientCertificate(cert);
}
-
+
IAzureGraphClient client = clientBuilder.Build();
// Step 1 - Add the certificate to the Service Principal (and set it as the preferred SAML signing certificate)
@@ -177,7 +177,45 @@ public void GraphClient_ServicePrincipal_AddGetRemove_ReturnSuccess(string testA
[IntegrationTestingTheory]
[InlineData("clientcert")]
[InlineData("clientsecret")]
- public void GraphClient_DiscoverApplicationIds_ReturnSuccess(string testAuthMethod)
+ public void GraphClient_DiscoverApplicationObjectIds_ReturnSuccess(string testAuthMethod)
+ {
+ // Arrange
+ const string password = "passwordpasswordpassword";
+ string certName = "SPTest" + Guid.NewGuid().ToString()[..6];
+ X509Certificate2 ssCert = GetSelfSignedCert(certName);
+ string b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
+
+ IntegrationTestingFact env = new();
+ IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
+ .WithTenantId(env.TenantId)
+ .WithApplicationId(env.ApplicationId)
+ .WithTargetObjectId(env.TargetApplicationObjectId);
+
+ if (testAuthMethod == "clientcert")
+ {
+ clientBuilder.WithClientSecret(env.ClientSecret);
+ }
+ else
+ {
+ var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
+ clientBuilder.WithClientCertificate(cert);
+ }
+
+ IAzureGraphClient client = clientBuilder.Build();
+
+ // Act
+ OperationResult> operationResult = client.DiscoverApplicationObjectIds();
+
+ // Assert
+ Assert.True(operationResult.Success);
+ Assert.NotNull(operationResult.Result);
+ Assert.True(operationResult.Result.Any());
+ }
+
+ [IntegrationTestingTheory]
+ [InlineData("clientcert")]
+ [InlineData("clientsecret")]
+ public void GraphClient_DiscoverServicePrincipalObjectIds_ReturnSuccess(string testAuthMethod)
{
// Arrange
const string password = "passwordpasswordpassword";
@@ -189,7 +227,7 @@ public void GraphClient_DiscoverApplicationIds_ReturnSuccess(string testAuthMeth
IAzureGraphClientBuilder clientBuilder = new GraphClient.Builder()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
- .WithTargetApplicationId(env.TargetApplicationId);
+ .WithTargetObjectId(env.TargetServicePrincipalObjectId);
if (testAuthMethod == "clientcert")
{
@@ -200,11 +238,11 @@ public void GraphClient_DiscoverApplicationIds_ReturnSuccess(string testAuthMeth
var cert = X509Certificate2.CreateFromPemFile(env.ClientCertificatePath);
clientBuilder.WithClientCertificate(cert);
}
-
+
IAzureGraphClient client = clientBuilder.Build();
// Act
- OperationResult> operationResult = client.DiscoverApplicationIds();
+ OperationResult> operationResult = client.DiscoverServicePrincipalObjectIds();
// Assert
Assert.True(operationResult.Success);
@@ -221,7 +259,7 @@ public static X509Certificate2 GetSelfSignedCert(string hostname)
SubjectAlternativeNameBuilder subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
subjectAlternativeNameBuilder.AddDnsName(hostname);
req.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build());
- req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
+ req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("2.5.29.32.0"), new Oid("1.3.6.1.5.5.7.3.1") }, false));
X509Certificate2 selfSignedCert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
@@ -245,7 +283,7 @@ static void ConfigureLogging()
LogHandler.Factory = LoggerFactory.Create(builder =>
{
- builder.AddNLog();
+ builder.AddNLog();
});
}
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs b/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
index e8be1d1..5941871 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
@@ -41,7 +41,7 @@ public IAzureGraphClientBuilder WithTenantId(string tenantId)
return this;
}
- public IAzureGraphClientBuilder WithTargetApplicationId(string applicationId)
+ public IAzureGraphClientBuilder WithTargetObjectId(string applicationId)
{
_targetApplicationId = applicationId;
return this;
@@ -80,7 +80,7 @@ public IAzureGraphClient Build()
ILogger _logger = LogHandler.GetClassLogger();
- public IEnumerable? ApplicationIdsAvailableOnFakeTenant { get; set; }
+ public IEnumerable? ObjectIdsAvailableOnFakeTenant { get; set; }
public Dictionary? CertificatesAvailableOnFakeTarget { get; set; }
public void AddApplicationCertificate(string certificateName, string certificateData)
@@ -104,14 +104,24 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
AddApplicationCertificate(certificateName, certificateData);
}
- public OperationResult> DiscoverApplicationIds()
+ public OperationResult> DiscoverApplicationObjectIds()
{
- if (ApplicationIdsAvailableOnFakeTenant == null)
+ if (ObjectIdsAvailableOnFakeTenant == null)
{
throw new Exception("Discover Application IDs method failure - no application ids set");
}
- return new OperationResult>(ApplicationIdsAvailableOnFakeTenant);
+ return new OperationResult>(ObjectIdsAvailableOnFakeTenant);
+ }
+
+ public OperationResult> DiscoverServicePrincipalObjectIds()
+ {
+ if (ObjectIdsAvailableOnFakeTenant == null)
+ {
+ throw new Exception("Discover Application IDs method failure - no application ids set");
+ }
+
+ return new OperationResult>(ObjectIdsAvailableOnFakeTenant);
}
public OperationResult> GetApplicationCertificates()
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs b/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs
index ee6e960..3d76de5 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs
@@ -21,7 +21,8 @@ public sealed class IntegrationTestingFact : FactAttribute
public string ClientSecret { get; private set; }
public string ClientCertificatePath { get; private set; }
- public string TargetApplicationId { get; private set; }
+ public string TargetApplicationObjectId { get; private set; }
+ public string TargetServicePrincipalObjectId { get; private set; }
public IntegrationTestingFact()
{
@@ -30,9 +31,10 @@ public IntegrationTestingFact()
ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty;
ClientCertificatePath = Environment.GetEnvironmentVariable("AZURE_PATH_TO_CLIENT_CERTIFICATE") ?? string.Empty;
- TargetApplicationId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_ID") ?? string.Empty;
+ TargetApplicationObjectId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_OBJECT_ID") ?? string.Empty;
+ TargetServicePrincipalObjectId = Environment.GetEnvironmentVariable("AZURE_TARGET_SERVICEPRINCIPAL_OBJECT_ID") ?? string.Empty;
- if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationId))
+ if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationObjectId) || string.IsNullOrEmpty(TargetApplicationObjectId))
{
Skip = "Integration testing environment variables are not set - Skipping test. Please run `make setup` to set the environment variables.";
}
@@ -46,7 +48,8 @@ public sealed class IntegrationTestingTheory : TheoryAttribute
public string ClientSecret { get; private set; }
public string ClientCertificatePath { get; private set; }
- public string TargetApplicationId { get; private set; }
+ public string TargetApplicationObjectId { get; private set; }
+ public string TargetServicePrincipalObjectId { get; private set; }
public IntegrationTestingTheory()
{
@@ -55,9 +58,10 @@ public IntegrationTestingTheory()
ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty;
ClientCertificatePath = Environment.GetEnvironmentVariable("AZURE_PATH_TO_CLIENT_CERTIFICATE") ?? string.Empty;
- TargetApplicationId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_ID") ?? string.Empty;
+ TargetApplicationObjectId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_OBJECT_ID") ?? string.Empty;
+ TargetServicePrincipalObjectId = Environment.GetEnvironmentVariable("AZURE_TARGET_SERVICEPRINCIPAL_OBJECT_ID") ?? string.Empty;
- if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationId))
+ if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationObjectId) || string.IsNullOrEmpty(TargetApplicationObjectId))
{
Skip = "Integration testing environment variables are not set - Skipping test. Please run `make setup` to set the environment variables.";
}
diff --git a/AzureEnterpriseApplicationOrchestrator/.DS_Store b/AzureEnterpriseApplicationOrchestrator/.DS_Store
deleted file mode 100644
index 414e25560c70e5d29556639791679b0555e046ac..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6148
zcmeHK&2AGh5FRI?-4H6}08%cMR^l2$3JR#=lBTT?TtF;>1E7#i0^PE;quqo^jUw$C
z-ho%(%8S6eaDs1a7i~60>Iot6MD}NU=Cju`{)vf5^k$KLliPk7!3$3YUD>^QW{JAi4
zEjMyNkM()8_Pt`ez$#OSZ;-2fi;w49h4XY`=l&&}#8EnJx4(%}xpHY^)7$iF-mBn9
z&4cM+I!k+ldSkKZ+_}~CckXtUO@Fc5x!v?T?cL?F>Rr2j^WKyGyZA(^5BPG3z}c&1%i31Jy15)I^X|?zkmO)l3dMsz
G2Yv#y&c-$X
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs
index 1528c30..aa3993b 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs
@@ -40,7 +40,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
JobResult result = new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId
+ JobHistoryId = config.JobHistoryId
};
List discoveredApplicationIds = new();
@@ -60,7 +60,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
try
{
- var operationResult = Client.DiscoverApplicationIds();
+ var operationResult = Client.DiscoverApplicationObjectIds();
if (!operationResult.Success)
{
result.FailureMessage += operationResult.ErrorMessage;
@@ -68,7 +68,8 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
continue;
}
discoveredApplicationIds.AddRange(operationResult.Result);
- }catch (Exception ex)
+ }
+ catch (Exception ex)
{
_logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
result.FailureMessage = ex.Message;
@@ -80,7 +81,8 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
{
callback(discoveredApplicationIds);
result.Result = OrchestratorJobStatusJobResult.Success;
- } catch (Exception ex)
+ }
+ catch (Exception ex)
{
_logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
result.FailureMessage = ex.Message;
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs
index bc1aff1..1fa1ee7 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs
@@ -40,7 +40,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
JobResult result = new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId
+ JobHistoryId = config.JobHistoryId
};
List discoveredApplicationIds = new();
@@ -60,7 +60,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
try
{
- var operationResult = Client.DiscoverApplicationIds();
+ var operationResult = Client.DiscoverServicePrincipalObjectIds();
if (!operationResult.Success)
{
result.FailureMessage += operationResult.ErrorMessage;
@@ -68,7 +68,8 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
continue;
}
discoveredApplicationIds.AddRange(operationResult.Result);
- }catch (Exception ex)
+ }
+ catch (Exception ex)
{
_logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
result.FailureMessage = ex.Message;
@@ -80,7 +81,8 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
{
callback(discoveredApplicationIds);
result.Result = OrchestratorJobStatusJobResult.Success;
- } catch (Exception ex)
+ }
+ catch (Exception ex)
{
_logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
result.FailureMessage = ex.Message;
diff --git a/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs b/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
index 73b3b78..0dfd6c2 100644
--- a/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
+++ b/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
@@ -29,18 +29,14 @@
namespace AzureEnterpriseApplicationOrchestrator.Client;
-public class GraphClient : IAzureGraphClient {
+public class GraphClient : IAzureGraphClient
+{
private ILogger _logger { get; set; }
private TokenCredential _credential { get; set; }
private string _tenantId { get; set; }
private GraphServiceClient _graphClient { get; set; }
- private string _targetApplicationId { get; set; }
-
- // In Azure, the application and service principal are separate objects bound by
- // a single Application ID.
- private string _applicationObjectId { get; set; }
- private string _servicePrincipalObjectId { get; set; }
+ private string _targetObjectId { get; set; }
// The Client can only be constructed by the Builder method
// unless they use the constructor that passes a pre-configured
@@ -65,7 +61,7 @@ public class Builder : IAzureGraphClientBuilder
private string _applicationId { get; set; }
private string _clientSecret { get; set; }
private X509Certificate2 _clientCertificate { get; set; }
- private string _targetApplicationId { get; set; }
+ private string _targetObjectId { get; set; }
private Uri _azureCloudEndpoint { get; set; }
public IAzureGraphClientBuilder WithTenantId(string tenantId)
@@ -74,9 +70,9 @@ public IAzureGraphClientBuilder WithTenantId(string tenantId)
return this;
}
- public IAzureGraphClientBuilder WithTargetApplicationId(string applicationId)
+ public IAzureGraphClientBuilder WithTargetObjectId(string objectId)
{
- _targetApplicationId = applicationId;
+ _targetObjectId = objectId;
return this;
}
@@ -100,7 +96,7 @@ public IAzureGraphClientBuilder WithClientCertificate(X509Certificate2 clientCer
public IAzureGraphClientBuilder WithAzureCloud(string azureCloud)
{
- if (string.IsNullOrWhiteSpace(azureCloud))
+ if (string.IsNullOrWhiteSpace(azureCloud))
{
azureCloud = "public";
}
@@ -133,23 +129,23 @@ public IAzureGraphClient Build()
DefaultAzureCredentialOptions credentialOptions = new DefaultAzureCredentialOptions
{
AuthorityHost = _azureCloudEndpoint,
- AdditionallyAllowedTenants = { "*" }
+ AdditionallyAllowedTenants = { "*" }
};
TokenCredential credential;
- if (!string.IsNullOrWhiteSpace(_clientSecret))
+ if (!string.IsNullOrWhiteSpace(_clientSecret))
{
credential = new ClientSecretCredential(
_tenantId, _applicationId, _clientSecret, credentialOptions
);
}
- else if (_clientCertificate != null)
+ else if (_clientCertificate != null)
{
credential = new ClientCertificateCredential(
_tenantId, _applicationId, _clientCertificate, credentialOptions
);
}
- else
+ else
{
throw new Exception("Client secret or client certificate must be provided.");
}
@@ -162,75 +158,13 @@ public IAzureGraphClient Build()
_client._graphClient = graphClient;
_client._credential = credential;
_client._tenantId = _tenantId;
- _client._targetApplicationId = _targetApplicationId;
+ _client._targetObjectId = _targetObjectId;
logger.LogTrace("Azure Resource Management client created.");
return _client;
}
}
- private string GetApplicationObjectId()
- {
- if (_applicationObjectId != null)
- {
- _logger.LogTrace($"Application object ID already set. Returning cached value. [{_applicationObjectId}]");
- return _applicationObjectId;
- }
-
- ApplicationCollectionResponse apps;
- try
- {
- apps = _graphClient.Applications.GetAsync(requestConfiguration =>
- {
- requestConfiguration.QueryParameters.Filter = $"(appId eq '{_targetApplicationId}')";
- requestConfiguration.QueryParameters.Top = 1;
- }).Result;
- } catch (AggregateException e)
- {
- _logger.LogError($"Unable to query MS Graph for Application \"{_targetApplicationId}\": {e}");
- throw;
- }
-
- if (apps?.Value == null || apps.Value.Count == 0 || string.IsNullOrEmpty(apps.Value.FirstOrDefault()?.Id))
- {
- throw new Exception($"Application with Application ID \"{_targetApplicationId}\" not found in tenant \"{_tenantId}\"");
- }
-
- _applicationObjectId = apps.Value.FirstOrDefault()?.Id;
- return _applicationObjectId;
- }
-
- private string GetServicePrincipalObjectId()
- {
- if (_servicePrincipalObjectId != null)
- {
- _logger.LogTrace($"Service principal object ID already set. Returning cached value. [{_servicePrincipalObjectId}]");
- return _servicePrincipalObjectId;
- }
-
- ServicePrincipalCollectionResponse sps;
- try
- {
- sps = _graphClient.ServicePrincipals.GetAsync(requestConfiguration =>
- {
- requestConfiguration.QueryParameters.Filter = $"(appId eq '{_targetApplicationId}')";
- requestConfiguration.QueryParameters.Top = 1;
- }).Result;
- } catch (AggregateException e)
- {
- _logger.LogError($"Unable to query MS Graph for ServicePrincipal \"{_targetApplicationId}\": {e}");
- throw;
- }
-
- if (sps?.Value == null || sps.Value.Count == 0 || string.IsNullOrEmpty(sps.Value.FirstOrDefault()?.Id))
- {
- throw new Exception($"Service Principal with Application ID \"{_targetApplicationId}\" not found in tenant \"{_tenantId}\"");
- }
-
- _servicePrincipalObjectId = sps.Value.FirstOrDefault()?.Id;
- return _servicePrincipalObjectId;
- }
-
public void AddApplicationCertificate(string certificateName, string certificateData)
{
// certificateData is a base64 encoded PFX certificate
@@ -241,7 +175,7 @@ public void AddApplicationCertificate(string certificateName, string certificate
// Calculate the SHA256 hash of the certificate's thumbprint
byte[] customKeyId = Encoding.UTF8.GetBytes(certificate.Thumbprint)[..32];
- _logger.LogDebug($"Adding certificate called \"{certificateName}\" to application ID \"{_targetApplicationId}\" (custom key ID {Encoding.UTF8.GetString(customKeyId)})");
+ _logger.LogDebug($"Adding certificate called \"{certificateName}\" to application ID \"{_targetObjectId}\" (custom key ID {Encoding.UTF8.GetString(customKeyId)})");
// Get the application object
Application application = GetApplication();
@@ -249,12 +183,12 @@ public void AddApplicationCertificate(string certificateName, string certificate
char[] certPem = PemEncoding.Write("CERTIFICATE", certificate.RawData);
// Update the application object
- _logger.LogDebug($"Updating application object for application ID \"{_targetApplicationId}\"");
+ _logger.LogDebug($"Updating application object for application ID \"{_targetObjectId}\"");
try
{
- _graphClient.Applications[GetApplicationObjectId()].PatchAsync(new Application
- {
- KeyCredentials = new List(DeepCopyKeyList(application.KeyCredentials))
+ _graphClient.Applications[_targetObjectId].PatchAsync(new Application
+ {
+ KeyCredentials = new List(DeepCopyKeyList(application.KeyCredentials))
{
new KeyCredential {
DisplayName = certificateName,
@@ -267,7 +201,7 @@ public void AddApplicationCertificate(string certificateName, string certificate
Key = System.Text.Encoding.UTF8.GetBytes(certPem)
}
}
- }).Wait();
+ }).Wait();
}
catch (AggregateException e)
{
@@ -299,13 +233,13 @@ public void RemoveApplicationCertificate(string certificateName)
keysToKeep.Add(keyCredential);
}
- _logger.LogDebug($"Updating application object for application ID \"{_targetApplicationId}\"");
+ _logger.LogDebug($"Updating application object for application ID \"{_targetObjectId}\"");
try
{
- _graphClient.Applications[GetApplicationObjectId()].PatchAsync(new Application
- {
- KeyCredentials = keysToKeep
- }).Wait();
+ _graphClient.Applications[_targetObjectId].PatchAsync(new Application
+ {
+ KeyCredentials = keysToKeep
+ }).Wait();
}
catch (AggregateException e)
{
@@ -336,18 +270,18 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
// Calculate the SHA256 hash of the certificate's thumbprint
byte[] customKeyId = Encoding.UTF8.GetBytes(certificate.Thumbprint)[..32];
- _logger.LogDebug($"Adding certificate called \"{certificateName}\" to application ID \"{_targetApplicationId}\" (custom key ID {Encoding.UTF8.GetString(customKeyId)})");
+ _logger.LogDebug($"Adding certificate called \"{certificateName}\" to application ID \"{_targetObjectId}\" (custom key ID {Encoding.UTF8.GetString(customKeyId)})");
// Create a GUID to represent the key ID and to link the key to the certificate
Guid privKeyGuid = Guid.NewGuid();
// Update the service principal object
- _logger.LogDebug($"Updating service principal object for application ID \"{_targetApplicationId}\"");
+ _logger.LogDebug($"Updating service principal object for application ID \"{_targetObjectId}\"");
try
{
- _graphClient.ServicePrincipals[GetServicePrincipalObjectId()].PatchAsync(new ServicePrincipal
- {
- KeyCredentials = new List()
+ _graphClient.ServicePrincipals[_targetObjectId].PatchAsync(new ServicePrincipal
+ {
+ KeyCredentials = new List()
{
new KeyCredential {
DisplayName = certificateName,
@@ -370,7 +304,7 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
Key = certificate.Export(X509ContentType.Pfx, certificatePassword)
}
},
- PasswordCredentials = new List()
+ PasswordCredentials = new List()
{
new PasswordCredential
{
@@ -381,8 +315,9 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
SecretText = certificatePassword,
}
}
- }).Wait();
- } catch (AggregateException e)
+ }).Wait();
+ }
+ catch (AggregateException e)
{
_logger.LogWarning($"Failed to update service principal object: {e}");
// TODO remove certificates to avoid leaving the service principal in a bad state
@@ -392,10 +327,10 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
// Update the preferred SAML certificate
try
{
- _graphClient.ServicePrincipals[GetServicePrincipalObjectId()].PatchAsync(new ServicePrincipal
- {
- PreferredTokenSigningKeyThumbprint = certificate.Thumbprint
- }).Wait();
+ _graphClient.ServicePrincipals[_targetObjectId].PatchAsync(new ServicePrincipal
+ {
+ PreferredTokenSigningKeyThumbprint = certificate.Thumbprint
+ }).Wait();
}
catch (AggregateException e)
{
@@ -449,15 +384,16 @@ public void RemoveServicePrincipalCertificate(string certificateName)
}
// Update the service principal object
- _logger.LogDebug($"Updating service principal object for application ID \"{_targetApplicationId}\"");
+ _logger.LogDebug($"Updating service principal object for application ID \"{_targetObjectId}\"");
try
{
- _graphClient.ServicePrincipals[GetServicePrincipalObjectId()].PatchAsync(new ServicePrincipal
- {
- KeyCredentials = keysToKeep,
- PasswordCredentials = passwordsToKeep
- });
- } catch (AggregateException e)
+ _graphClient.ServicePrincipals[_targetObjectId].PatchAsync(new ServicePrincipal
+ {
+ KeyCredentials = keysToKeep,
+ PasswordCredentials = passwordsToKeep
+ });
+ }
+ catch (AggregateException e)
{
_logger.LogWarning($"Failed to update service principal object with updated certificate list: {e}");
throw;
@@ -477,10 +413,10 @@ public bool ServicePrincipalCertificateExists(string certificateName)
return servicePrincipal.KeyCredentials != null && servicePrincipal.KeyCredentials.Any(c => c.DisplayName == certificateName);
}
- OperationResult> IAzureGraphClient.DiscoverApplicationIds()
+ public OperationResult> DiscoverApplicationObjectIds()
{
- List appIds = new();
- OperationResult> result = new(appIds);
+ List oids = new();
+ OperationResult> result = new(oids);
_logger.LogDebug($"Retrieving application registrations for tenant ID \"{_tenantId}\"");
ApplicationCollectionResponse apps;
@@ -488,7 +424,7 @@ OperationResult> IAzureGraphClient.DiscoverApplicationIds()
{
apps = _graphClient.Applications.GetAsync((requestConfiguration) =>
{
- requestConfiguration.QueryParameters.Top = 999;
+ requestConfiguration.QueryParameters.Top = 999;
}).Result;
}
catch (AggregateException e)
@@ -507,58 +443,60 @@ OperationResult> IAzureGraphClient.DiscoverApplicationIds()
{
_logger.LogDebug($"Found application \"{app.DisplayName}\" ({app.Id})");
- if (app.AppId == null)
+ if (app.Id == null)
{
- _logger.LogWarning($"Application \"{app.DisplayName}\" ({app.Id}) does not have an AppID");
- result.AddRuntimeErrorMessage($"Application \"{app.DisplayName}\" ({app.Id}) does not have an AppID");
+ _logger.LogWarning($"Application \"{app.DisplayName}\" ({app.Id}) does not have an Object ID");
+ result.AddRuntimeErrorMessage($"Application \"{app.DisplayName}\" ({app.Id}) does not have an Object ID");
continue;
}
- appIds.Add(app.AppId);
+ oids.Add($"{app.Id} ({app.DisplayName})");
}
return result;
}
- public IEnumerable DiscoverApplicationIds()
+ public OperationResult> DiscoverServicePrincipalObjectIds()
{
- List appIds = new();
+ List oids = new();
+ OperationResult> result = new(oids);
- _logger.LogDebug($"Retrieving application registrations for tenant ID \"{_tenantId}\"");
- ApplicationCollectionResponse apps;
+ _logger.LogDebug($"Retrieving Service Principals for tenant ID \"{_tenantId}\"");
+ ServicePrincipalCollectionResponse sps;
try
{
- apps = _graphClient.Applications.GetAsync((requestConfiguration) =>
- {
- requestConfiguration.QueryParameters.Top = 999;
- }).Result;
+ sps = _graphClient.ServicePrincipals.GetAsync((requestConfiguration) =>
+ {
+ requestConfiguration.QueryParameters.Top = 999;
+ }).Result;
}
catch (AggregateException e)
{
- _logger.LogError($"Unable to retrieve application registrations for tenant ID \"{_tenantId}\": {e}");
+ _logger.LogError($"Unable to retrieve Service Principals for tenant ID \"{_tenantId}\": {e}");
throw;
}
- if (apps?.Value == null || apps.Value.Count == 0)
+ if (sps?.Value == null || sps.Value.Count == 0)
{
- _logger.LogWarning($"No application registrations found for tenant ID \"{_tenantId}\"");
- return appIds;
+ _logger.LogWarning($"No Service Principals found for tenant ID \"{_tenantId}\"");
+ return result;
}
- foreach (Application app in apps.Value)
+ foreach (ServicePrincipal sp in sps.Value)
{
- _logger.LogDebug($"Found application \"{app.DisplayName}\" ({app.Id})");
+ _logger.LogDebug($"Found SP \"{sp.DisplayName}\" ({sp.Id})");
- if (app.AppId == null)
+ if (sp.AppId == null)
{
- _logger.LogWarning($"Application \"{app.DisplayName}\" ({app.Id}) does not have an AppID");
+ _logger.LogWarning($"Service Principal \"{sp.DisplayName}\" ({sp.Id}) does not have an AppID");
+ result.AddRuntimeErrorMessage($"Service Principal \"{sp.DisplayName}\" ({sp.Id}) does not have an AppID");
continue;
}
- appIds.Add(app.AppId);
+ oids.Add($"{sp.Id} ({sp.DisplayName})");
}
- return appIds;
+ return result;
}
private OperationResult> InventoryFromKeyCredentials(List keyCredentials)
@@ -568,7 +506,7 @@ private OperationResult> InventoryFromKeyCrede
if (keyCredentials == null || keyCredentials.Count == 0)
{
- _logger.LogWarning($"No key credentials found for application ID \"{_targetApplicationId}\"");
+ _logger.LogWarning($"No key credentials found for application ID \"{_targetObjectId}\"");
return result;
}
@@ -581,7 +519,7 @@ private OperationResult> InventoryFromKeyCrede
// track the ones that we failed to serialize, and remove them from the map when we do find the certificate.
// Finally, we'll log a warning for any certificates that we failed to retrieve.
Dictionary failedCertificateMap = new Dictionary();
-
+
// Create a map to track certificates that we're confident have a private key entry in Azure.
// Azure will never return the Private Key with the Graph API, but Keyfactor Command uses
// the presence of a private key to determine how Certificate Renewal should be handled.
@@ -591,7 +529,7 @@ private OperationResult> InventoryFromKeyCrede
foreach (KeyCredential keyCredential in keyCredentials)
{
string customKeyIdentifier = Encoding.UTF8.GetString(keyCredential.CustomKeyIdentifier);
-
+
if (!string.IsNullOrWhiteSpace(keyCredential.Usage) && keyCredential.Usage.Equals("Sign", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug($"Certificate with CustomKeyIdentifier \"{customKeyIdentifier}\" has a private key entry");
@@ -628,10 +566,10 @@ private OperationResult> InventoryFromKeyCrede
CurrentInventoryItem inventoryItem = new CurrentInventoryItem()
{
Alias = keyCredential.DisplayName,
- PrivateKeyEntry = false,
- ItemStatus = OrchestratorInventoryItemStatus.Unknown,
- UseChainLevel = true,
- Certificates = certificates
+ PrivateKeyEntry = false,
+ ItemStatus = OrchestratorInventoryItemStatus.Unknown,
+ UseChainLevel = true,
+ Certificates = certificates
};
_logger.LogDebug($"Found certificate called \"{keyCredential.DisplayName}\" ({customKeyIdentifier})");
@@ -649,7 +587,7 @@ private OperationResult> InventoryFromKeyCrede
_logger.LogWarning(failedCertificateMap[key]);
result.AddRuntimeErrorMessage(failedCertificateMap[key]);
}
-
+
foreach (string key in privateKeyMap.Keys)
{
if (inventoryItems.ContainsKey(key))
@@ -663,22 +601,22 @@ private OperationResult> InventoryFromKeyCrede
protected Application GetApplication()
{
- _logger.LogDebug($"Retrieving application for application ID \"{_targetApplicationId}\"");
+ _logger.LogDebug($"Retrieving application for application ID \"{_targetObjectId}\"");
Application app;
try
{
- app = _graphClient.Applications[GetApplicationObjectId()].GetAsync(
+ app = _graphClient.Applications[_targetObjectId].GetAsync(
requestConfiguration =>
{
- requestConfiguration.QueryParameters.Select = new[] { "id","appId","keyCredentials","passwordCredentials" };
+ requestConfiguration.QueryParameters.Select = new[] { "id", "appId", "keyCredentials", "passwordCredentials" };
}
).Result;
}
catch (AggregateException ex)
{
- _logger.LogError($"Error retrieving application for application ID \"{_targetApplicationId}\": {ex}");
+ _logger.LogError($"Error retrieving application for application ID \"{_targetObjectId}\": {ex}");
throw;
}
@@ -687,20 +625,20 @@ protected Application GetApplication()
protected ServicePrincipal GetServicePrincipal()
{
- _logger.LogDebug($"Retrieving service principal for application ID \"{_targetApplicationId}\"");
+ _logger.LogDebug($"Retrieving service principal for application ID \"{_targetObjectId}\"");
ServicePrincipal sp;
try
{
- sp = _graphClient.ServicePrincipals[GetServicePrincipalObjectId()].GetAsync(requestConfiguration =>
+ sp = _graphClient.ServicePrincipals[_targetObjectId].GetAsync(requestConfiguration =>
{
- requestConfiguration.QueryParameters.Select = new[] { "id","appId","keyCredentials","passwordCredentials" };
+ requestConfiguration.QueryParameters.Select = new[] { "id", "appId", "keyCredentials", "passwordCredentials" };
}).Result;
}
catch (AggregateException ex)
{
- _logger.LogError($"Error retrieving service principal for application ID \"{_targetApplicationId}\": {ex}");
+ _logger.LogError($"Error retrieving service principal for application ID \"{_targetObjectId}\": {ex}");
throw;
}
@@ -717,13 +655,13 @@ protected List DeepCopyKeyList(List keyCredentials
else
{
deepKeyList = keyCredentials.Select(keyCredential => new KeyCredential
- {
- CustomKeyIdentifier = keyCredential.CustomKeyIdentifier,
- DisplayName = keyCredential.DisplayName,
- Key = keyCredential.Key,
- Type = keyCredential.Type,
- Usage = keyCredential.Usage,
- })
+ {
+ CustomKeyIdentifier = keyCredential.CustomKeyIdentifier,
+ DisplayName = keyCredential.DisplayName,
+ Key = keyCredential.Key,
+ Type = keyCredential.Type,
+ Usage = keyCredential.Usage,
+ })
.ToList();
}
@@ -741,14 +679,14 @@ protected IEnumerable DeepCopyPasswordList(List new PasswordCredential
- {
- CustomKeyIdentifier = passwordCredential.CustomKeyIdentifier,
- DisplayName = passwordCredential.DisplayName,
- EndDateTime = passwordCredential.EndDateTime,
- KeyId = passwordCredential.KeyId,
- SecretText = passwordCredential.SecretText,
- StartDateTime = passwordCredential.StartDateTime,
- })
+ {
+ CustomKeyIdentifier = passwordCredential.CustomKeyIdentifier,
+ DisplayName = passwordCredential.DisplayName,
+ EndDateTime = passwordCredential.EndDateTime,
+ KeyId = passwordCredential.KeyId,
+ SecretText = passwordCredential.SecretText,
+ StartDateTime = passwordCredential.StartDateTime,
+ })
.ToList();
}
@@ -783,5 +721,4 @@ protected static X509Certificate2 SerializeCertificate(string certificateData, s
byte[] rawData = Convert.FromBase64String(certificateData);
return new X509Certificate2(rawData, password, X509KeyStorageFlags.Exportable);
}
-
}
diff --git a/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs b/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs
index 2043c67..3f3148a 100644
--- a/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs
+++ b/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs
@@ -21,7 +21,7 @@ namespace AzureEnterpriseApplicationOrchestrator.Client;
public interface IAzureGraphClientBuilder
{
public IAzureGraphClientBuilder WithTenantId(string tenantId);
- public IAzureGraphClientBuilder WithTargetApplicationId(string applicationId);
+ public IAzureGraphClientBuilder WithTargetObjectId(string applicationId);
public IAzureGraphClientBuilder WithApplicationId(string applicationId);
public IAzureGraphClientBuilder WithClientSecret(string clientSecret);
public IAzureGraphClientBuilder WithClientCertificate(X509Certificate2 clientCertificate);
@@ -64,5 +64,6 @@ public interface IAzureGraphClient
public bool ServicePrincipalCertificateExists(string certificateName);
// Discovery
- public OperationResult> DiscoverApplicationIds();
+ public OperationResult> DiscoverApplicationObjectIds();
+ public OperationResult> DiscoverServicePrincipalObjectIds();
}
diff --git a/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs b/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs
index 3474155..95131f5 100644
--- a/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs
+++ b/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs
@@ -47,11 +47,15 @@ public GraphJobClientBuilder WithCertificateStoreDetails(CertificateSt
_logger.LogTrace($"Builder - StorePath => TargetApplicationId: {details.StorePath}");
_logger.LogTrace($"Builder - ServerUsername => ApplicationId: {properties.ServerUsername}");
_logger.LogTrace($"Builder - AzureCloud => AzureCloud: {properties.AzureCloud}");
-
+
+ // The Discovery Job returns Object IDs in the format ` ()`.
+ // We split out the first part to get the Object ID.
+ string normalizedObjectID = details.StorePath.Split(" ")[0];
+
_builder
.WithTenantId(details.ClientMachine)
.WithApplicationId(properties.ServerUsername)
- .WithTargetApplicationId(details.StorePath)
+ .WithTargetObjectId(normalizedObjectID)
.WithAzureCloud(properties.AzureCloud);
if (string.IsNullOrWhiteSpace(properties.ClientCertificate))
@@ -95,15 +99,16 @@ private X509Certificate2 SerializeClientCertificate(string clientCertificate, st
{
// clientCertificate is a Base64 encoded certificate that's either PEM or PKCS#12 encoded.
// We expect that it includes a private key compatible with the dotnet standard crypto libraries.
-
+
byte[] rawCertBytes = Convert.FromBase64String(clientCertificate);
X509Certificate2 serializedCertificate = null;
-
+
// Try to serialize the certificate without any special handling
try
{
serializedCertificate = new X509Certificate2(rawCertBytes, password, X509KeyStorageFlags.Exportable);
- if (serializedCertificate.HasPrivateKey) {
+ if (serializedCertificate.HasPrivateKey)
+ {
_logger.LogTrace("Successfully serialized certificate using standard X509Certificate2");
return serializedCertificate;
}
@@ -129,7 +134,7 @@ private X509Certificate2 SerializePemCertificateAndKey(string clientCertificate,
{
_logger.LogDebug($"Attempting to serialize client certificate and private key from PEM encoding");
ReadOnlySpan utf8Cert = Encoding.UTF8.GetChars(Convert.FromBase64String(clientCertificate));
-
+
_logger.LogTrace("Finding all PEM objects in ClientCertificate");
ReadOnlySpan certificate = new char[0];
@@ -164,7 +169,7 @@ private X509Certificate2 SerializePemCertificateAndKey(string clientCertificate,
// Copy over the slice before the start of the range
utf8Cert.Slice(0, start).CopyTo(newUtf8Cert);
// Copy over the slice after the end of the range
- utf8Cert.Slice(end).CopyTo(newUtf8Cert.AsSpan(start));
+ utf8Cert.Slice(end).CopyTo(newUtf8Cert.AsSpan(start));
utf8Cert = newUtf8Cert;
}
From 2c03c6fdd1ce70a3eb68be1dac7ae1e83e2cac41 Mon Sep 17 00:00:00 2001
From: Keyfactor
Date: Tue, 24 Sep 2024 23:09:14 +0000
Subject: [PATCH 02/10] Update generated docs
---
README.md | 228 +++++++++++-------------------------------------------
1 file changed, 44 insertions(+), 184 deletions(-)
diff --git a/README.md b/README.md
index 7f950ea..0b813e3 100644
--- a/README.md
+++ b/README.md
@@ -46,224 +46,84 @@ The Azure App Registration and Enterprise Application Universal Orchestrator ext
> 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.
## Installation
-Before installing the Azure App Registration and Enterprise Application Universal Orchestrator extension, it's recommended to install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command.
-The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 2 Certificate Store Types. Depending on your use case, you may elect to install one, or all of these Certificate Store Types. An overview for each type is linked below:
-* [Azure App Registration (Application)](docs/azureapp.md)
-* [Azure Enterprise Application (Service Principal)](docs/azuresp.md)
+Before installing the Azure App Registration and Enterprise Application Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command.
-Azure App Registration (Application)
-
-
-1. Follow the [requirements section](docs/azureapp.md#requirements) to configure a Service Account and grant necessary API permissions.
-
- Requirements
-
- #### Azure Service Principal (Graph API Authentication)
-
- The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, Client Secret authentication is supported. The Service Principal must have the following API Permission:
- - **_Microsoft Graph Application Permissions_**:
- - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
-
- > For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
-
- Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
-
- ##### Client Certificate or Client Secret
-
- Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
-
- * **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
- * **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
-
- The certificate can be in either of the following formats:
- * Base64-encoded PKCS#12 (PFX) with a matching private key.
- * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
-
- If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
- > **Creating and Formatting a Client Certificate using Keyfactor Command**
- >
- > To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
- >
- > 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
- > 2. Select a **Template** that supports Client Authentication as an extended key usage.
- > 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
- > 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
- > 5. Make a note of the password on the next page - it won't be shown again.
- > 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
- > * If you downloaded the certificate in PEM format, use the commands below:
- >
- > ```shell
- > # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
- > cat
- >
- > # Separate the certificate from the private key
- > openssl x509 -in -out pubkeycert.pem
- >
- > # Base64 encode the certificate and private key
- > cat | base64 > clientcertkeypair.pem.base64
- > ```
- >
- > * If you downloaded the certificate in PFX format, use the commands below:
- >
- > ```shell
- > # Export the certificate from the PFX file
- > openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
- >
- > # Base64 encode the PFX file
- > cat | base64 > clientcert.pfx.base64
- > ```
- > 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
- >
- > You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+1. **Create Certificate Store Types in Keyfactor Command**
+The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 2 Certificate Store Types. Depending on your use case, you may elect to install one, or all of these Certificate Store Types.
- #### Azure App Registration (Application)
+ Azure App Registration (Application)
- ##### Application Certificates
- Application certificates are used for client authentication and are typically public key only. No additional configuration in Azure is necessary to manage Application certificates since all App Registrations can contain any number of [Certificates and Secrets](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-credentials). Unless the Discovery job is used, you should collect the Application IDs for each App Registration that contains certificates to be managed.
+ > More information on the Azure App Registration (Application) Certificate Store Type can be found [here](docs/azureapp.md).
-
-
-2. Create Certificate Store Types for the Azure App Registration and Enterprise Application Orchestrator extension.
-
- * **Using kfutil**:
+ * **Create AzureApp using kfutil**:
```shell
# Azure App Registration (Application)
kfutil store-types create AzureApp
```
- * **Manually**:
- * [Azure App Registration (Application)](docs/azureapp.md#certificate-store-type-configuration)
-
-3. Install the Azure App Registration and Enterprise Application Universal Orchestrator extension.
-
- * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command:
-
- ```shell
- # Windows Server
- kfutil orchestrator extension -e azure-application-orchestrator@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions"
-
- # Linux
- kfutil orchestrator extension -e azure-application-orchestrator@latest --out "/opt/keyfactor/orchestrator/extensions"
- ```
-
- * **Manually**: Follow the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions) to install the latest [Azure App Registration and Enterprise Application Universal Orchestrator extension](https://github.com/Keyfactor/azure-application-orchestrator/releases/latest).
-
-4. Create new certificate stores in Keyfactor Command for the Sample Universal Orchestrator extension.
-
- * [Azure App Registration (Application)](docs/azureapp.md#certificate-store-configuration)
-
-
-
-
-Azure Enterprise Application (Service Principal)
-
-
-1. Follow the [requirements section](docs/azuresp.md#requirements) to configure a Service Account and grant necessary API permissions.
-
- Requirements
-
- #### Azure Service Principal (Graph API Authentication)
-
- The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, Client Secret authentication is supported. The Service Principal must have the following API Permission:
- - **_Microsoft Graph Application Permissions_**:
- - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
-
- > For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
-
- Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
+ * **Create AzureApp manually in the Command UI**:
+
+ Refer to the [Azure App Registration (Application)](docs/azureapp.md#certificate-store-type-configuration) creation docs.
+
- ##### Client Certificate or Client Secret
+ Azure Enterprise Application (Service Principal)
- Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
- * **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
- * **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
+ > More information on the Azure Enterprise Application (Service Principal) Certificate Store Type can be found [here](docs/azuresp.md).
- The certificate can be in either of the following formats:
- * Base64-encoded PKCS#12 (PFX) with a matching private key.
- * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
+ * **Create AzureSP using kfutil**:
- If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+ ```shell
+ # Azure Enterprise Application (Service Principal)
+ kfutil store-types create AzureSP
+ ```
- > **Creating and Formatting a Client Certificate using Keyfactor Command**
- >
- > To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
- >
- > 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
- > 2. Select a **Template** that supports Client Authentication as an extended key usage.
- > 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
- > 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
- > 5. Make a note of the password on the next page - it won't be shown again.
- > 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
- > * If you downloaded the certificate in PEM format, use the commands below:
- >
- > ```shell
- > # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
- > cat
- >
- > # Separate the certificate from the private key
- > openssl x509 -in -out pubkeycert.pem
- >
- > # Base64 encode the certificate and private key
- > cat | base64 > clientcertkeypair.pem.base64
- > ```
- >
- > * If you downloaded the certificate in PFX format, use the commands below:
- >
- > ```shell
- > # Export the certificate from the PFX file
- > openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
- >
- > # Base64 encode the PFX file
- > cat | base64 > clientcert.pfx.base64
- > ```
- > 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
- >
- > You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+ * **Create AzureSP manually in the Command UI**:
+
+ Refer to the [Azure Enterprise Application (Service Principal)](docs/azuresp.md#certificate-store-type-configuration) creation docs.
+
- #### Enterprise Application (Service Principal)
+2. **Download the latest Azure App Registration and Enterprise Application Universal Orchestrator extension from GitHub.**
- ##### Service Principal Certificates
+ On the [Azure App Registration and Enterprise Application Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/azure-application-orchestrator/releases/latest), click the `azure-application-orchestrator` asset to download the zip archive. Unzip the archive containing extension assemblies to a known location.
- Service Principal certificates are typically used for SAML Token signing. Service Principals are created from Enterprise Applications, and will mostly be configured with a variation of Microsoft's [SAML-based single sign-on](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal) documentation. For more information on the mechanics of the Service Principal certificate management capabilities of this extension, please see the [mechanics](#extension-mechanics) section.
+3. **Locate the Universal Orchestrator extensions directory.**
-
+ * **Default on Windows** - `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions`
+ * **Default on Linux** - `/opt/keyfactor/orchestrator/extensions`
+
+4. **Create a new directory for the Azure App Registration and Enterprise Application Universal Orchestrator extension inside the extensions directory.**
+
+ Create a new directory called `azure-application-orchestrator`.
+ > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory.
-2. Create Certificate Store Types for the Azure App Registration and Enterprise Application Orchestrator extension.
+5. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `azure-application-orchestrator` directory.**
- * **Using kfutil**:
+6. **Restart the Universal Orchestrator service.**
- ```shell
- # Azure Enterprise Application (Service Principal)
- kfutil store-types create AzureSP
- ```
+ Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm).
- * **Manually**:
- * [Azure Enterprise Application (Service Principal)](docs/azuresp.md#certificate-store-type-configuration)
-3. Install the Azure App Registration and Enterprise Application Universal Orchestrator extension.
-
- * **Using kfutil**: On the server that that hosts the Universal Orchestrator, run the following command:
- ```shell
- # Windows Server
- kfutil orchestrator extension -e azure-application-orchestrator@latest --out "C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions"
+> The above installation steps can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions).
- # Linux
- kfutil orchestrator extension -e azure-application-orchestrator@latest --out "/opt/keyfactor/orchestrator/extensions"
- ```
+## Configuration and Usage
- * **Manually**: Follow the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions) to install the latest [Azure App Registration and Enterprise Application Universal Orchestrator extension](https://github.com/Keyfactor/azure-application-orchestrator/releases/latest).
+The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 2 Certificate Store Types, each of which implements different functionality. Refer to the individual instructions below for each Certificate Store Type that you deemed necessary for your use case from the installation section.
-4. Create new certificate stores in Keyfactor Command for the Sample Universal Orchestrator extension.
+Azure App Registration (Application)
- * [Azure Enterprise Application (Service Principal)](docs/azuresp.md#certificate-store-configuration)
+1. Refer to the [requirements section](docs/azureapp.md#requirements) to ensure all prerequisites are met before using the Azure App Registration (Application) Certificate Store Type.
+2. Create new [Azure App Registration (Application)](docs/azureapp.md#certificate-store-configuration) Certificate Stores in Keyfactor Command.
+
+Azure Enterprise Application (Service Principal)
+1. Refer to the [requirements section](docs/azuresp.md#requirements) to ensure all prerequisites are met before using the Azure Enterprise Application (Service Principal) Certificate Store Type.
+2. Create new [Azure Enterprise Application (Service Principal)](docs/azuresp.md#certificate-store-configuration) Certificate Stores in Keyfactor Command.
From ca30904fe5ab0090059855b9c8c29f3e6a9595bf Mon Sep 17 00:00:00 2001
From: Hayden Roszell
Date: Thu, 3 Oct 2024 12:26:00 -0700
Subject: [PATCH 03/10] chore(storetypesv2): Implement V2 Certificat Store
Types that interpret the Store Path as the Object ID
Signed-off-by: Hayden Roszell
---
.../AzureApp.cs | 20 +-
.../AzureApp2.cs | 561 ++++++++++++
.../AzureSP.cs | 6 +-
.../AzureSP2.cs | 568 ++++++++++++
.../FakeClient.cs | 43 +-
.../IntegrationTestingFact.cs | 5 +-
.../JobClientBuilder.cs | 162 +++-
.../AzureApp2Jobs/Discovery.cs | 113 +++
.../AzureApp2Jobs/Inventory.cs | 94 ++
.../AzureApp2Jobs/Management.cs | 149 ++++
.../AzureAppJobs/Discovery.cs | 4 +-
.../AzureAppJobs/Inventory.cs | 15 +-
.../AzureAppJobs/Management.cs | 14 +-
.../AzureSP2Jobs/Discovery.cs | 113 +++
.../AzureSP2Jobs/Inventory.cs | 94 ++
.../AzureSP2Jobs/Management.cs | 151 ++++
.../AzureSPJobs/Discovery.cs | 4 +-
.../AzureSPJobs/Inventory.cs | 15 +-
.../AzureSPJobs/Management.cs | 14 +-
.../Client/GraphClient.cs | 255 ++++--
.../Client/IAzureGraphClient.cs | 19 +-
.../GraphJobClientBuilder.cs | 76 +-
.../manifest.json | 78 +-
README.md | 818 +++++++++++++++++-
docs/azureapp.md | 206 -----
docs/azuresp.md | 206 -----
docsource/azureapp.md | 76 +-
docsource/azureapp2.md | 21 +
docsource/azuresp.md | 75 +-
docsource/azuresp2.md | 21 +
docsource/content.md | 80 ++
.../AzureApp-advanced-store-type-dialog.png | Bin 41666 -> 41694 bytes
.../AzureApp-basic-store-type-dialog.png | Bin 54630 -> 54648 bytes
...ureApp-custom-fields-store-type-dialog.png | Bin 40175 -> 40207 bytes
.../AzureApp2-advanced-store-type-dialog.png | Bin 0 -> 41694 bytes
.../AzureApp2-basic-store-type-dialog.png | Bin 0 -> 55531 bytes
...reApp2-custom-fields-store-type-dialog.png | Bin 0 -> 42405 bytes
.../AzureSP-advanced-store-type-dialog.png | Bin 41666 -> 41691 bytes
.../AzureSP-basic-store-type-dialog.png | Bin 54914 -> 54938 bytes
...zureSP-custom-fields-store-type-dialog.png | Bin 40175 -> 40207 bytes
.../AzureSP2-advanced-store-type-dialog.png | Bin 0 -> 41691 bytes
.../AzureSP2-basic-store-type-dialog.png | Bin 0 -> 55451 bytes
...ureSP2-custom-fields-store-type-dialog.png | Bin 0 -> 42405 bytes
docsource/overview.md | 6 -
integration-manifest.json | 148 +++-
45 files changed, 3475 insertions(+), 755 deletions(-)
create mode 100644 AzureEnterpriseApplicationOrchestrator.Tests/AzureApp2.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator.Tests/AzureSP2.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Discovery.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Inventory.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Management.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Discovery.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Inventory.cs
create mode 100644 AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Management.cs
delete mode 100644 docs/azureapp.md
delete mode 100644 docs/azuresp.md
create mode 100644 docsource/azureapp2.md
create mode 100644 docsource/azuresp2.md
create mode 100644 docsource/content.md
create mode 100644 docsource/images/AzureApp2-advanced-store-type-dialog.png
create mode 100644 docsource/images/AzureApp2-basic-store-type-dialog.png
create mode 100644 docsource/images/AzureApp2-custom-fields-store-type-dialog.png
create mode 100644 docsource/images/AzureSP2-advanced-store-type-dialog.png
create mode 100644 docsource/images/AzureSP2-basic-store-type-dialog.png
create mode 100644 docsource/images/AzureSP2-custom-fields-store-type-dialog.png
delete mode 100644 docsource/overview.md
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs b/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs
index 68615c2..c55e588 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp.cs
@@ -48,7 +48,7 @@ public void AzureApp_Inventory_IntegrationTest_ReturnSuccess()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
- .WithTargetObjectId(env.TargetApplicationObjectId)
+ .WithTargetApplicationApplicationId(env.TargetApplicationApplicationId)
.Build();
// Set up the inventory job configuration
@@ -57,7 +57,7 @@ public void AzureApp_Inventory_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetApplicationObjectId,
+ StorePath = env.TargetApplicationApplicationId,
Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
}
};
@@ -118,14 +118,14 @@ public void AzureApp_Inventory_ProcessJob_ValidClient_ReturnSuccess()
// Act
JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
- {
- // Assert
- Assert.Equal(1, inventoryItems.Count());
- Assert.Equal("test", inventoryItems.First().Alias);
+ {
+ // Assert
+ Assert.Equal(1, inventoryItems.Count());
+ Assert.Equal("test", inventoryItems.First().Alias);
- _logger.LogInformation("AzureApp_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
- return true;
- });
+ _logger.LogInformation("AzureApp_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
+ });
// Assert
Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
@@ -481,7 +481,7 @@ public void AzureApp_Management_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetApplicationObjectId,
+ StorePath = env.TargetApplicationApplicationId,
Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
},
JobCertificate = new ManagementJobCertificate
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp2.cs b/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp2.cs
new file mode 100644
index 0000000..5188fa2
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/AzureApp2.cs
@@ -0,0 +1,561 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Security.Cryptography.X509Certificates;
+using AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+using NLog.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.Tests;
+
+public class AzureEnterpriseApplicationOrchestrator_AzureApp2
+{
+ ILogger _logger { get; set; }
+
+ public AzureEnterpriseApplicationOrchestrator_AzureApp2()
+ {
+ ConfigureLogging();
+
+ _logger = LogHandler.GetClassLogger();
+ }
+
+ [IntegrationTestingFact]
+ public void AzureApp2_Inventory_IntegrationTest_ReturnSuccess()
+ {
+ // Arrange
+ string certName = "AppTest" + Guid.NewGuid().ToString()[..6];
+ X509Certificate2 ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(certName);
+ string b64Cert = Convert.ToBase64String(ssCert.Export(X509ContentType.Cert));
+
+ IntegrationTestingFact env = new();
+
+ IAzureGraphClient client = new GraphClient.Builder()
+ .WithTenantId(env.TenantId)
+ .WithApplicationId(env.ApplicationId)
+ .WithClientSecret(env.ClientSecret)
+ .WithTargetObjectId(env.TargetApplicationObjectId)
+ .Build();
+
+ // Set up the inventory job configuration
+ var config = new InventoryJobConfiguration
+ {
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = env.TenantId,
+ StorePath = env.TargetApplicationObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ }
+ };
+
+ var inventory = new Inventory();
+
+ // Create a certificate in the Application
+ client.AddApplicationCertificate(certName, b64Cert);
+
+ // Act
+ JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
+ {
+ // Assert
+ Assert.NotNull(inventoryItems);
+ Assert.NotEmpty(inventoryItems);
+
+ _logger.LogInformation("AzureApp2_Inventory_IntegrationTest_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+
+ // Clean up
+ client.RemoveApplicationCertificate(certName);
+ }
+
+ [Fact]
+ public void AzureApp2_Inventory_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient
+ {
+ CertificatesAvailableOnFakeTarget = new Dictionary
+ {
+ { "test", "test" }
+ }
+ };
+
+ // Set up the inventory job with the fake client
+ var inventory = new Inventory
+ {
+ Client = client
+ };
+
+ // Set up the inventory job configuration
+ var config = new InventoryJobConfiguration
+ {
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = "test",
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
+ {
+ // Assert
+ Assert.Equal(1, inventoryItems.Count());
+ Assert.Equal("test", inventoryItems.First().Alias);
+
+ _logger.LogInformation("AzureApp2_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ }
+
+ [Fact]
+ public void AzureApp2_Inventory_ProcessJob_InvalidClient_ReturnFailure()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient();
+
+ // Set up the inventory job with the fake client
+ var inventory = new Inventory
+ {
+ Client = client
+ };
+
+ // Set up the inventory job configuration
+ var config = new InventoryJobConfiguration
+ {
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = "test",
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ },
+ JobHistoryId = 1
+ };
+
+ bool callbackCalled = false;
+
+ // Act
+ JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
+ {
+ callbackCalled = true;
+
+ // Assert
+ Assert.True(false, "Callback should not be called");
+ return true;
+ });
+
+ // Assert
+ Assert.False(callbackCalled);
+ Assert.Equal(OrchestratorJobStatusJobResult.Failure, result.Result);
+
+ _logger.LogInformation("AzureApp2_Inventory_ProcessJob_InvalidClient_ReturnFailure - Success");
+ }
+
+ [IntegrationTestingFact]
+ public void AzureApp2_Discovery_IntegrationTest_ReturnSuccess()
+ {
+ // Arrange
+ IntegrationTestingFact env = new();
+
+ // Set up the discovery job configuration
+ var config = new DiscoveryJobConfiguration
+ {
+ ClientMachine = env.TenantId,
+ ServerUsername = env.ApplicationId,
+ ServerPassword = env.ClientSecret,
+ JobProperties = new Dictionary
+ {
+ { "dirs", env.TenantId }
+ }
+ };
+
+ var discovery = new Discovery();
+
+ // Act
+ JobResult result = discovery.ProcessJob(config, (discoveredApplicationIds) =>
+ {
+ // Assert
+ Assert.NotNull(discoveredApplicationIds);
+ Assert.NotEmpty(discoveredApplicationIds);
+
+ _logger.LogInformation("AzureApp2_Discovery_IntegrationTest_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ }
+
+ [Fact]
+ public void AzureApp2_Discovery_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient
+ {
+ ObjectIdsAvailableOnFakeTenant = new List { "test" }
+ };
+
+ // Set up the discovery job with the fake client
+ var discovery = new Discovery
+ {
+ Client = client
+ };
+
+ // Set up the discovery job configuration
+ var config = new DiscoveryJobConfiguration
+ {
+ ClientMachine = "fake-tenant-id",
+ ServerUsername = "fake-application-id",
+ ServerPassword = "fake-client-secret",
+ JobProperties = new Dictionary
+ {
+ { "dirs", "fake-tenant-id" }
+ }
+ };
+
+ // Act
+ JobResult result = discovery.ProcessJob(config, (discoveredApplicationIds) =>
+ {
+ // Assert
+ Assert.Equal(1, discoveredApplicationIds.Count());
+ Assert.Equal("test", discoveredApplicationIds.First());
+
+ _logger.LogInformation("Discovery_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ _logger.LogInformation("AzureApp2_Discovery_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [Fact]
+ public void AzureApp2_Discovery_ProcessJob_InvalidClient_ReturnFailure()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient();
+
+ // Set up the discovery job with the fake client
+ var discovery = new Discovery
+ {
+ Client = client
+ };
+
+ // Set up the discovery job configuration
+ var config = new DiscoveryJobConfiguration
+ {
+ ClientMachine = "fake-tenant-id",
+ ServerUsername = "fake-application-id",
+ ServerPassword = "fake-client-secret",
+ JobProperties = new Dictionary
+ {
+ { "dirs", "fake-tenant-id" }
+ }
+ };
+
+ bool callbackCalled = false;
+
+ // Act
+ JobResult result = discovery.ProcessJob(config, (discoveredApplicationIds) =>
+ {
+ callbackCalled = true;
+
+ // Assert
+ Assert.True(false, "Callback should not be called");
+ return true;
+ });
+
+ // Assert
+ Assert.False(callbackCalled);
+ Assert.Equal(OrchestratorJobStatusJobResult.Failure, result.Result);
+
+ _logger.LogInformation("AzureApp2_Discovery_ProcessJob_InvalidClient_ReturnFailure - Success");
+ }
+
+ [Fact]
+ public void AzureApp2_ManagementAdd_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ FakeClient client = new FakeClient();
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = "test",
+ Contents = "test-certificate-data"
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+ Assert.NotNull(client.CertificatesAvailableOnFakeTarget);
+ if (client.CertificatesAvailableOnFakeTarget != null)
+ {
+ Assert.True(client.CertificatesAvailableOnFakeTarget.ContainsKey("test"));
+ }
+
+ _logger.LogInformation("AzureApp2_ManagementAdd_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("", "test-password")]
+ public void AzureApp2_ManagementAdd_ProcessJob_InvalidJobConfig_ReturnFailure(string alias, string pkPassword)
+ {
+ // Arrange
+ FakeClient client = new FakeClient();
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = alias,
+ Contents = "test-certificate-data",
+ PrivateKeyPassword = pkPassword
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Failure, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+
+ _logger.LogInformation("AzureApp2_ManagementAdd_ProcessJob_InvalidJobConfig_ReturnFailure - Success");
+ }
+
+ [Fact]
+ public void AzureApp2_ManagementRemove_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ FakeClient client = new FakeClient
+ {
+ CertificatesAvailableOnFakeTarget = new Dictionary
+ {
+ { "test", "test" }
+ }
+ };
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Remove,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = "test",
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+ if (client.CertificatesAvailableOnFakeTarget != null)
+ {
+ Assert.False(client.CertificatesAvailableOnFakeTarget.ContainsKey("test"));
+ }
+
+ _logger.LogInformation("AzureApp2_ManagementRemove_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [Fact]
+ public void AzureApp2_ManagementReplace_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ FakeClient client = new FakeClient
+ {
+ CertificatesAvailableOnFakeTarget = new Dictionary
+ {
+ { "test", "original-cert-data" }
+ }
+ };
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ Overwrite = true,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = "test",
+ Contents = "new-certificate-data"
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+ if (client.CertificatesAvailableOnFakeTarget != null)
+ {
+ Assert.True(client.CertificatesAvailableOnFakeTarget.ContainsKey("test"));
+ Assert.Equal("new-certificate-data", client.CertificatesAvailableOnFakeTarget["test"]);
+ }
+
+ _logger.LogInformation("AzureApp2_ManagementReplace_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [IntegrationTestingFact]
+ public void AzureApp2_Management_IntegrationTest_ReturnSuccess()
+ {
+ // Arrange
+ IntegrationTestingFact env = new();
+
+ string testHostname = "azureapplicationUnitTest.com";
+ string certName = "AppTest" + Guid.NewGuid().ToString()[..6];
+
+ X509Certificate2 ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(testHostname);
+
+ string b64Cert = Convert.ToBase64String(ssCert.Export(X509ContentType.Cert));
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = env.TenantId,
+ StorePath = env.TargetApplicationObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ },
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = certName,
+ Contents = b64Cert
+ },
+ };
+
+ var management = new Management();
+
+ // Act
+ // This will process a Management Add job
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ // Arrange
+
+ ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(testHostname);
+
+ b64Cert = Convert.ToBase64String(ssCert.Export(X509ContentType.Cert));
+
+ config.OperationType = CertStoreOperationType.Add;
+ config.Overwrite = true;
+ config.JobCertificate = new ManagementJobCertificate
+ {
+ Alias = certName,
+ Contents = b64Cert
+ };
+
+ // Act
+ // This will process a Management Replace job
+ result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ // Arrange
+ config.OperationType = CertStoreOperationType.Remove;
+ config.JobCertificate = new ManagementJobCertificate
+ {
+ Alias = certName,
+ };
+
+ // Act
+ // This will process a Management Remove job
+ result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ _logger.LogInformation("AzureApp2_Management_IntegrationTest_ReturnSuccess - Success");
+ }
+
+ static void ConfigureLogging()
+ {
+ var config = new NLog.Config.LoggingConfiguration();
+
+ // Targets where to log to: File and Console
+ var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
+ logconsole.Layout = @"${date:format=HH\:mm\:ss} ${logger} [${level}] - ${message}";
+
+ // Rules for mapping loggers to targets
+ config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, logconsole);
+
+ // Apply config
+ NLog.LogManager.Configuration = config;
+
+ LogHandler.Factory = LoggerFactory.Create(builder =>
+ {
+ builder.AddNLog();
+ });
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs b/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs
index c78f740..e55b295 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP.cs
@@ -49,7 +49,7 @@ public void AzureSP_Inventory_IntegrationTest_ReturnSuccess()
.WithTenantId(env.TenantId)
.WithApplicationId(env.ApplicationId)
.WithClientSecret(env.ClientSecret)
- .WithTargetObjectId(env.TargetServicePrincipalObjectId)
+ .WithTargetServicePrincipalApplicationId(env.TargetApplicationApplicationId)
.Build();
// Set up the inventory job configuration
@@ -58,7 +58,7 @@ public void AzureSP_Inventory_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetServicePrincipalObjectId,
+ StorePath = env.TargetApplicationApplicationId,
Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
}
};
@@ -486,7 +486,7 @@ public void AzureSP_Management_IntegrationTest_ReturnSuccess()
CertificateStoreDetails = new CertificateStore
{
ClientMachine = env.TenantId,
- StorePath = env.TargetServicePrincipalObjectId,
+ StorePath = env.TargetApplicationApplicationId,
Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
},
JobCertificate = new ManagementJobCertificate
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP2.cs b/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP2.cs
new file mode 100644
index 0000000..b49a841
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/AzureSP2.cs
@@ -0,0 +1,568 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Security.Cryptography.X509Certificates;
+using AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+using NLog.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.Tests;
+
+public class AzureEnterpriseApplicationOrchestrator_AzureSP2
+{
+ ILogger _logger { get; set; }
+
+ public AzureEnterpriseApplicationOrchestrator_AzureSP2()
+ {
+ ConfigureLogging();
+
+ _logger = LogHandler.GetClassLogger();
+ }
+
+ [IntegrationTestingFact]
+ public void AzureSP2_Inventory_IntegrationTest_ReturnSuccess()
+ {
+ // Arrange
+ const string password = "passwordpasswordpassword";
+ string certName = "SPTest" + Guid.NewGuid().ToString()[..6];
+ X509Certificate2 ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(certName);
+ string b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
+
+ IntegrationTestingFact env = new();
+
+ IAzureGraphClient client = new GraphClient.Builder()
+ .WithTenantId(env.TenantId)
+ .WithApplicationId(env.ApplicationId)
+ .WithClientSecret(env.ClientSecret)
+ .WithTargetObjectId(env.TargetServicePrincipalObjectId)
+ .Build();
+
+ // Set up the inventory job configuration
+ var config = new InventoryJobConfiguration
+ {
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = env.TenantId,
+ StorePath = env.TargetServicePrincipalObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ }
+ };
+
+ var inventory = new Inventory();
+
+ // Create a certificate in the Application
+ client.AddServicePrincipalCertificate(certName, b64PfxSslCert, password);
+
+ // Act
+ JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
+ {
+ // Assert
+ Assert.NotNull(inventoryItems);
+ Assert.NotEmpty(inventoryItems);
+
+ _logger.LogInformation("AzureSP2_Inventory_IntegrationTest_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+
+ // Clean up
+ client.RemoveServicePrincipalCertificate(certName);
+ }
+
+ [Fact]
+ public void AzureSP2_Inventory_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient
+ {
+ CertificatesAvailableOnFakeTarget = new Dictionary
+ {
+ { "test", "test" }
+ }
+ };
+
+ // Set up the inventory job with the fake client
+ var inventory = new Inventory
+ {
+ Client = client
+ };
+
+ // Set up the inventory job configuration
+ var config = new InventoryJobConfiguration
+ {
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = "test",
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
+ {
+ // Assert
+ Assert.Equal(1, inventoryItems.Count());
+ Assert.Equal("test", inventoryItems.First().Alias);
+
+ _logger.LogInformation("AzureSP2_Inventory_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ }
+
+ [Fact]
+ public void AzureSP2_Inventory_ProcessJob_InvalidClient_ReturnFailure()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient();
+
+ // Set up the inventory job with the fake client
+ var inventory = new Inventory
+ {
+ Client = client
+ };
+
+ // Set up the inventory job configuration
+ var config = new InventoryJobConfiguration
+ {
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = "test",
+ StorePath = "test",
+ Properties = "{\"ServerUsername\":\"test\",\"ServerPassword\":\"test\",\"AzureCloud\":\"test\"}"
+ },
+ JobHistoryId = 1
+ };
+
+ bool callbackCalled = false;
+
+ // Act
+ JobResult result = inventory.ProcessJob(config, (inventoryItems) =>
+ {
+ callbackCalled = true;
+
+ // Assert
+ Assert.True(false, "Callback should not be called");
+ return true;
+ });
+
+ // Assert
+ Assert.False(callbackCalled);
+ Assert.Equal(OrchestratorJobStatusJobResult.Failure, result.Result);
+
+ _logger.LogInformation("AzureSP2_Inventory_ProcessJob_InvalidClient_ReturnFailure - Success");
+ }
+
+ [IntegrationTestingFact]
+ public void AzureSP2_Discovery_IntegrationTest_ReturnSuccess()
+ {
+ // Arrange
+ IntegrationTestingFact env = new();
+
+ // Set up the discovery job configuration
+ var config = new DiscoveryJobConfiguration
+ {
+ ClientMachine = env.TenantId,
+ ServerUsername = env.ApplicationId,
+ ServerPassword = env.ClientSecret,
+ JobProperties = new Dictionary
+ {
+ { "dirs", env.TenantId }
+ }
+ };
+
+ var discovery = new Discovery();
+
+ // Act
+ JobResult result = discovery.ProcessJob(config, (discoveredApplicationIds) =>
+ {
+ // Assert
+ Assert.NotNull(discoveredApplicationIds);
+ Assert.NotEmpty(discoveredApplicationIds);
+
+ _logger.LogInformation("AzureSP2_Discovery_IntegrationTest_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ }
+
+ [Fact]
+ public void AzureSP2_Discovery_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient
+ {
+ ObjectIdsAvailableOnFakeTenant = new List { "test" }
+ };
+
+ // Set up the discovery job with the fake client
+ var discovery = new Discovery
+ {
+ Client = client
+ };
+
+ // Set up the discovery job configuration
+ var config = new DiscoveryJobConfiguration
+ {
+ ClientMachine = "fake-tenant-id",
+ ServerUsername = "fake-application-id",
+ ServerPassword = "fake-client-secret",
+ JobProperties = new Dictionary
+ {
+ { "dirs", "fake-tenant-id" }
+ }
+ };
+
+ // Act
+ JobResult result = discovery.ProcessJob(config, (discoveredApplicationIds) =>
+ {
+ // Assert
+ Assert.Equal(1, discoveredApplicationIds.Count());
+ Assert.Equal("test", discoveredApplicationIds.First());
+
+ _logger.LogInformation("Discovery_ProcessJob_ValidClient_ReturnSuccess - Success");
+ return true;
+ });
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ _logger.LogInformation("AzureSP2_Discovery_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [Fact]
+ public void AzureSP2_Discovery_ProcessJob_InvalidClient_ReturnFailure()
+ {
+ // Arrange
+ IAzureGraphClient client = new FakeClient();
+
+ // Set up the discovery job with the fake client
+ var discovery = new Discovery
+ {
+ Client = client
+ };
+
+ // Set up the discovery job configuration
+ var config = new DiscoveryJobConfiguration
+ {
+ ClientMachine = "fake-tenant-id",
+ ServerUsername = "fake-application-id",
+ ServerPassword = "fake-client-secret",
+ JobProperties = new Dictionary
+ {
+ { "dirs", "fake-tenant-id" }
+ }
+ };
+
+ bool callbackCalled = false;
+
+ // Act
+ JobResult result = discovery.ProcessJob(config, (discoveredApplicationIds) =>
+ {
+ callbackCalled = true;
+
+ // Assert
+ Assert.True(false, "Callback should not be called");
+ return true;
+ });
+
+ // Assert
+ Assert.False(callbackCalled);
+ Assert.Equal(OrchestratorJobStatusJobResult.Failure, result.Result);
+
+ _logger.LogInformation("AzureSP2_Discovery_ProcessJob_InvalidClient_ReturnFailure - Success");
+ }
+
+ [Fact]
+ public void AzureSP2_ManagementAdd_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ FakeClient client = new FakeClient();
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = "test",
+ Contents = "test-certificate-data",
+ PrivateKeyPassword = "test-password"
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+ Assert.NotNull(client.CertificatesAvailableOnFakeTarget);
+ if (client.CertificatesAvailableOnFakeTarget != null)
+ {
+ Assert.True(client.CertificatesAvailableOnFakeTarget.ContainsKey("test"));
+ }
+
+ _logger.LogInformation("AzureSP2_ManagementAdd_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [Theory]
+ [InlineData("test", "")]
+ [InlineData("", "test-password")]
+ [InlineData("", "")]
+ public void AzureSP2_ManagementAdd_ProcessJob_InvalidJobConfig_ReturnFailure(string alias, string pkPassword)
+ {
+ // Arrange
+ FakeClient client = new FakeClient();
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = alias,
+ Contents = "test-certificate-data",
+ PrivateKeyPassword = pkPassword
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Failure, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+
+ _logger.LogInformation("AzureSP2_ManagementAdd_ProcessJob_InvalidJobConfig_ReturnFailure - Success");
+ }
+
+ [Fact]
+ public void AzureSP2_ManagementRemove_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ FakeClient client = new FakeClient
+ {
+ CertificatesAvailableOnFakeTarget = new Dictionary
+ {
+ { "test", "test" }
+ }
+ };
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Remove,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = "test",
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+ if (client.CertificatesAvailableOnFakeTarget != null)
+ {
+ Assert.False(client.CertificatesAvailableOnFakeTarget.ContainsKey("test"));
+ }
+
+ _logger.LogInformation("AzureSP2_ManagementRemove_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [Fact]
+ public void AzureSP2_ManagementReplace_ProcessJob_ValidClient_ReturnSuccess()
+ {
+ // Arrange
+ FakeClient client = new FakeClient
+ {
+ CertificatesAvailableOnFakeTarget = new Dictionary
+ {
+ { "test", "original-cert-data" }
+ }
+ };
+
+ // Set up the management job with the fake client
+ var management = new Management
+ {
+ Client = client
+ };
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ Overwrite = true,
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = "test",
+ Contents = "new-certificate-data",
+ PrivateKeyPassword = "test-password"
+ },
+ JobHistoryId = 1
+ };
+
+ // Act
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+ Assert.Equal(1, result.JobHistoryId);
+ if (client.CertificatesAvailableOnFakeTarget != null)
+ {
+ Assert.True(client.CertificatesAvailableOnFakeTarget.ContainsKey("test"));
+ Assert.Equal("new-certificate-data", client.CertificatesAvailableOnFakeTarget["test"]);
+ }
+
+ _logger.LogInformation("AzureSP2_ManagementReplace_ProcessJob_ValidClient_ReturnSuccess - Success");
+ }
+
+ [IntegrationTestingFact]
+ public void AzureSP2_Management_IntegrationTest_ReturnSuccess()
+ {
+ // Arrange
+ IntegrationTestingFact env = new();
+
+ string testHostname = "azureapplicationUnitTest.com";
+ string certName = "AppTest" + Guid.NewGuid().ToString()[..6];
+ string password = "password";
+
+ X509Certificate2 ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(testHostname);
+
+ string b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
+
+ // Set up the management job configuration
+ var config = new ManagementJobConfiguration
+ {
+ OperationType = CertStoreOperationType.Add,
+ CertificateStoreDetails = new CertificateStore
+ {
+ ClientMachine = env.TenantId,
+ StorePath = env.TargetServicePrincipalObjectId,
+ Properties = $"{{\"ServerUsername\":\"{env.ApplicationId}\",\"ServerPassword\":\"{env.ClientSecret}\",\"AzureCloud\":\"\"}}"
+ },
+ JobCertificate = new ManagementJobCertificate
+ {
+ Alias = certName,
+ Contents = b64PfxSslCert,
+ PrivateKeyPassword = password
+ },
+ };
+
+ var management = new Management();
+
+ // Act
+ // This will process a Management Add job
+ JobResult result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ // Arrange
+
+ ssCert = AzureEnterpriseApplicationOrchestrator_Client.GetSelfSignedCert(testHostname);
+
+ b64PfxSslCert = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
+
+ config.OperationType = CertStoreOperationType.Add;
+ config.Overwrite = true;
+ config.JobCertificate = new ManagementJobCertificate
+ {
+ Alias = certName,
+ Contents = b64PfxSslCert,
+ PrivateKeyPassword = password
+ };
+
+ // Act
+ // This will process a Management Replace job
+ result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ // Arrange
+ config.OperationType = CertStoreOperationType.Remove;
+ config.JobCertificate = new ManagementJobCertificate
+ {
+ Alias = certName,
+ };
+
+ // Act
+ // This will process a Management Remove job
+ result = management.ProcessJob(config);
+
+ // Assert
+ Assert.Equal(OrchestratorJobStatusJobResult.Success, result.Result);
+
+ _logger.LogInformation("AzureSP2_Management_IntegrationTest_ReturnSuccess - Success");
+ }
+
+ static void ConfigureLogging()
+ {
+ var config = new NLog.Config.LoggingConfiguration();
+
+ // Targets where to log to: File and Console
+ var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
+ logconsole.Layout = @"${date:format=HH\:mm\:ss} ${logger} [${level}] - ${message}";
+
+ // Rules for mapping loggers to targets
+ config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, logconsole);
+
+ // Apply config
+ NLog.LogManager.Configuration = config;
+
+ LogHandler.Factory = LoggerFactory.Create(builder =>
+ {
+ builder.AddNLog();
+ });
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs b/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
index 5941871..a3c4775 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/FakeClient.cs
@@ -23,13 +23,14 @@ namespace AzureEnterpriseApplicationOrchestrator.Tests;
public class FakeClient : IAzureGraphClient
{
-
public class FakeBuilder : IAzureGraphClientBuilder
{
private FakeClient _client = new FakeClient();
public string? _tenantId { get; set; }
- public string? _targetApplicationId { get; set; }
+ public string? _targetObjectId { get; set; }
+ public string? _targetApplicationApplicationId { get; set; }
+ public string? _targetServicePrincipalApplicationId { get; set; }
public string? _applicationId { get; set; }
public string? _clientSecret { get; set; }
public X509Certificate2? _clientCertificate { get; set; }
@@ -41,9 +42,21 @@ public IAzureGraphClientBuilder WithTenantId(string tenantId)
return this;
}
- public IAzureGraphClientBuilder WithTargetObjectId(string applicationId)
+ public IAzureGraphClientBuilder WithTargetObjectId(string objectId)
+ {
+ _targetObjectId = objectId;
+ return this;
+ }
+
+ public IAzureGraphClientBuilder WithTargetServicePrincipalApplicationId(string applicationId)
+ {
+ _targetServicePrincipalApplicationId = applicationId;
+ return this;
+ }
+
+ public IAzureGraphClientBuilder WithTargetApplicationApplicationId(string applicationId)
{
- _targetApplicationId = applicationId;
+ _targetApplicationApplicationId = applicationId;
return this;
}
@@ -108,13 +121,33 @@ public OperationResult> DiscoverApplicationObjectIds()
{
if (ObjectIdsAvailableOnFakeTenant == null)
{
- throw new Exception("Discover Application IDs method failure - no application ids set");
+ throw new Exception("Discover Object IDs method failure - no application ids set");
}
return new OperationResult>(ObjectIdsAvailableOnFakeTenant);
}
public OperationResult> DiscoverServicePrincipalObjectIds()
+ {
+ if (ObjectIdsAvailableOnFakeTenant == null)
+ {
+ throw new Exception("Discover Object IDs method failure - no application ids set");
+ }
+
+ return new OperationResult>(ObjectIdsAvailableOnFakeTenant);
+ }
+
+ public OperationResult> DiscoverApplicationApplicationIds()
+ {
+ if (ObjectIdsAvailableOnFakeTenant == null)
+ {
+ throw new Exception("Discover Application IDs method failure - no application ids set");
+ }
+
+ return new OperationResult>(ObjectIdsAvailableOnFakeTenant);
+ }
+
+ public OperationResult> DiscoverServicePrincipalApplicationIds()
{
if (ObjectIdsAvailableOnFakeTenant == null)
{
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs b/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs
index 3d76de5..9ccb238 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/IntegrationTestingFact.cs
@@ -21,6 +21,8 @@ public sealed class IntegrationTestingFact : FactAttribute
public string ClientSecret { get; private set; }
public string ClientCertificatePath { get; private set; }
+
+ public string TargetApplicationApplicationId { get; private set; }
public string TargetApplicationObjectId { get; private set; }
public string TargetServicePrincipalObjectId { get; private set; }
@@ -31,10 +33,11 @@ public IntegrationTestingFact()
ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty;
ClientCertificatePath = Environment.GetEnvironmentVariable("AZURE_PATH_TO_CLIENT_CERTIFICATE") ?? string.Empty;
+ TargetApplicationApplicationId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_ID") ?? string.Empty;
TargetApplicationObjectId = Environment.GetEnvironmentVariable("AZURE_TARGET_APPLICATION_OBJECT_ID") ?? string.Empty;
TargetServicePrincipalObjectId = Environment.GetEnvironmentVariable("AZURE_TARGET_SERVICEPRINCIPAL_OBJECT_ID") ?? string.Empty;
- if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationObjectId) || string.IsNullOrEmpty(TargetApplicationObjectId))
+ if (string.IsNullOrEmpty(TenantId) || string.IsNullOrEmpty(ApplicationId) || string.IsNullOrEmpty(ClientSecret) || string.IsNullOrEmpty(TargetApplicationApplicationId) || string.IsNullOrEmpty(TargetApplicationObjectId) || string.IsNullOrEmpty(TargetApplicationObjectId))
{
Skip = "Integration testing environment variables are not set - Skipping test. Please run `make setup` to set the environment variables.";
}
diff --git a/AzureEnterpriseApplicationOrchestrator.Tests/JobClientBuilder.cs b/AzureEnterpriseApplicationOrchestrator.Tests/JobClientBuilder.cs
index cad3f39..5f6261a 100644
--- a/AzureEnterpriseApplicationOrchestrator.Tests/JobClientBuilder.cs
+++ b/AzureEnterpriseApplicationOrchestrator.Tests/JobClientBuilder.cs
@@ -25,7 +25,7 @@
public class AzureEnterpriseApplicationOrchestrator_JobClientBuilder
{
- ILogger _logger { get; set;}
+ ILogger _logger { get; set; }
public AzureEnterpriseApplicationOrchestrator_JobClientBuilder()
{
@@ -34,8 +34,11 @@ public AzureEnterpriseApplicationOrchestrator_JobClientBuilder()
_logger = LogHandler.GetClassLogger();
}
- [Fact]
- public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientSecret_BuildValidClient()
+ [Theory]
+ [InlineData("AzureApp")]
+ [InlineData("AzureSP")]
+ [InlineData("Unsupported")]
+ public void GraphJobClientBuilderV1_ValidCertificateStoreConfigWithClientSecret_BuildValidClient(string storetype)
{
// Verify that the GraphJobClientBuilder uses the certificate store configuration
// provided by Keyfactor Command/the Universal Orchestrator correctly as required
@@ -49,22 +52,42 @@ public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientSecret_Bu
CertificateStore fakeCertificateStoreDetails = new()
{
ClientMachine = "fake-tenant-id",
- StorePath = "fake-azure-target-application-id",
+ StorePath = "fake-azure-target-id",
Properties = "{\"ServerUsername\":\"fake-azure-application-id\",\"ServerPassword\":\"fake-azure-client-secret\",\"AzureCloud\":\"fake-azure-cloud\"}"
};
+ bool thrown = false;
+
// Act
- IAzureGraphClient fakeAppGatewayClient = jobClientBuilderWithFakeBuilder
- .WithCertificateStoreDetails(fakeCertificateStoreDetails)
- .Build();
+ try
+ {
+ jobClientBuilderWithFakeBuilder
+ .WithV1CertificateStoreDetails(fakeCertificateStoreDetails, storetype)
+ .Build();
+ }
+ catch (Exception)
+ {
+ if (storetype == "AzureApp" || storetype == "AzureSP") throw;
+ thrown = true;
+ }
+
// Assert
+ if (!thrown && storetype == "Unsupported") throw new Exception("Expected failure");
+ if (thrown && storetype == "Unsupported")
+ {
+ _logger.LogInformation("GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient - Success");
+ return;
+ }
// IAzureGraphClient doesn't require any of the properties set by the builder to be exposed
// since the production Build() method creates an Azure Resource Manager client.
// But, our builder is fake and exposes the properties we need to test (via the FakeBuilder class).
Assert.Equal("fake-tenant-id", jobClientBuilderWithFakeBuilder._builder._tenantId);
- Assert.Equal("fake-azure-target-application-id", jobClientBuilderWithFakeBuilder._builder._targetApplicationId);
+ if (storetype == "AzureApp")
+ Assert.Equal("fake-azure-target-id", jobClientBuilderWithFakeBuilder._builder._targetApplicationApplicationId);
+ if (storetype == "AzureSP")
+ Assert.Equal("fake-azure-target-id", jobClientBuilderWithFakeBuilder._builder._targetServicePrincipalApplicationId);
Assert.Equal("fake-azure-application-id", jobClientBuilderWithFakeBuilder._builder._applicationId);
Assert.Equal("fake-azure-client-secret", jobClientBuilderWithFakeBuilder._builder._clientSecret);
Assert.Equal("fake-azure-cloud", jobClientBuilderWithFakeBuilder._builder._azureCloudEndpoint);
@@ -72,11 +95,11 @@ public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientSecret_Bu
_logger.LogInformation("GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient - Success");
}
- [IntegrationTestingTheory]
+ [Theory]
[InlineData("pkcs12")]
[InlineData("pem")]
[InlineData("encryptedPem")]
- public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientCertificate_BuildValidClient(string certificateFormat)
+ public void GraphJobClientBuilderV1_ValidCertificateStoreConfigWithClientCertificate_BuildValidClient(string certificateFormat)
{
// Verify that the GraphJobClientBuilder uses the certificate store configuration
// provided by Keyfactor Command/the Universal Orchestrator correctly as required
@@ -123,7 +146,7 @@ public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientCertifica
// Act
IAzureGraphClient fakeAppGatewayClient = jobClientBuilderWithFakeBuilder
- .WithCertificateStoreDetails(fakeCertificateStoreDetails)
+ .WithV1CertificateStoreDetails(fakeCertificateStoreDetails, "AzureApp")
.Build();
// Assert
@@ -132,7 +155,116 @@ public void GraphJobClientBuilder_ValidCertificateStoreConfigWithClientCertifica
// since the production Build() method creates an Azure Resource Manager client.
// But, our builder is fake and exposes the properties we need to test (via the FakeBuilder class).
Assert.Equal("fake-tenant-id", jobClientBuilderWithFakeBuilder._builder._tenantId);
- Assert.Equal("fake-azure-target-application-id", jobClientBuilderWithFakeBuilder._builder._targetApplicationId);
+ Assert.Equal("fake-azure-target-application-id", jobClientBuilderWithFakeBuilder._builder._targetApplicationApplicationId);
+ Assert.Equal("fake-azure-application-id", jobClientBuilderWithFakeBuilder._builder._applicationId);
+ Assert.Equal("fake-azure-cloud", jobClientBuilderWithFakeBuilder._builder._azureCloudEndpoint);
+ Assert.Equal(ssCert.GetCertHash(), jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetCertHash());
+ Assert.NotNull(jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetRSAPrivateKey());
+ Assert.Equal(jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem(), ssCert.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem());
+
+ _logger.LogInformation("GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient - Success");
+ }
+
+ [Fact]
+ public void GraphJobClientBuilderV2_ValidCertificateStoreConfigWithClientSecret_BuildValidClient()
+ {
+ // Verify that the GraphJobClientBuilder uses the certificate store configuration
+ // provided by Keyfactor Command/the Universal Orchestrator correctly as required
+ // by the IAzureGraphClientBuilder interface.
+
+ // Arrange
+ GraphJobClientBuilder jobClientBuilderWithFakeBuilder = new();
+
+ // Set up the certificate store with names that correspond to how we expect them to be interpreted by
+ // the builder
+ CertificateStore fakeCertificateStoreDetails = new()
+ {
+ ClientMachine = "fake-tenant-id",
+ StorePath = "fake-azure-object-id",
+ Properties = "{\"ServerUsername\":\"fake-azure-application-id\",\"ServerPassword\":\"fake-azure-client-secret\",\"AzureCloud\":\"fake-azure-cloud\"}"
+ };
+
+ // Act
+ jobClientBuilderWithFakeBuilder
+ .WithV2CertificateStoreDetails(fakeCertificateStoreDetails)
+ .Build();
+
+
+ // Assert
+
+ // IAzureGraphClient doesn't require any of the properties set by the builder to be exposed
+ // since the production Build() method creates an Azure Resource Manager client.
+ // But, our builder is fake and exposes the properties we need to test (via the FakeBuilder class).
+ Assert.Equal("fake-tenant-id", jobClientBuilderWithFakeBuilder._builder._tenantId);
+ Assert.Equal("fake-azure-object-id", jobClientBuilderWithFakeBuilder._builder._targetObjectId);
+ Assert.Equal("fake-azure-application-id", jobClientBuilderWithFakeBuilder._builder._applicationId);
+ Assert.Equal("fake-azure-client-secret", jobClientBuilderWithFakeBuilder._builder._clientSecret);
+ Assert.Equal("fake-azure-cloud", jobClientBuilderWithFakeBuilder._builder._azureCloudEndpoint);
+
+ _logger.LogInformation("GraphJobClientBuilder_ValidCertificateStoreConfig_BuildValidClient - Success");
+ }
+
+ [Theory]
+ [InlineData("pkcs12")]
+ [InlineData("pem")]
+ [InlineData("encryptedPem")]
+ public void GraphJobClientBuilderV2_ValidCertificateStoreConfigWithClientCertificate_BuildValidClient(string certificateFormat)
+ {
+ // Verify that the GraphJobClientBuilder uses the certificate store configuration
+ // provided by Keyfactor Command/the Universal Orchestrator correctly as required
+ // by the IAzureGraphClientBuilder interface.
+
+ // Arrange
+ GraphJobClientBuilder jobClientBuilderWithFakeBuilder = new();
+
+ string password = "passwordpasswordpassword";
+ string certName = "SPTest" + Guid.NewGuid().ToString()[..6];
+ X509Certificate2 ssCert = GetSelfSignedCert(certName);
+
+ string b64ClientCertificate;
+ if (certificateFormat == "pkcs12")
+ {
+ b64ClientCertificate = Convert.ToBase64String(ssCert.Export(X509ContentType.Pfx, password));
+ }
+ else if (certificateFormat == "pem")
+ {
+ string pemCert = ssCert.ExportCertificatePem();
+ string keyPem = ssCert.GetRSAPrivateKey()!.ExportPkcs8PrivateKeyPem();
+ b64ClientCertificate = Convert.ToBase64String(Encoding.UTF8.GetBytes(keyPem + '\n' + pemCert));
+ password = "";
+ }
+ else
+ {
+ PbeParameters pbeParameters = new PbeParameters(
+ PbeEncryptionAlgorithm.Aes256Cbc,
+ HashAlgorithmName.SHA384,
+ 300_000);
+ string pemCert = ssCert.ExportCertificatePem();
+ string keyPem = ssCert.GetRSAPrivateKey()!.ExportEncryptedPkcs8PrivateKeyPem(password.ToCharArray(), pbeParameters);
+ b64ClientCertificate = Convert.ToBase64String(Encoding.UTF8.GetBytes(keyPem + '\n' + pemCert));
+ }
+
+ // Set up the certificate store with names that correspond to how we expect them to be interpreted by
+ // the builder
+ CertificateStore fakeCertificateStoreDetails = new()
+ {
+ ClientMachine = "fake-tenant-id",
+ StorePath = "fake-azure-target-object-id",
+ Properties = $@"{{""ServerUsername"": ""fake-azure-application-id"",""ClientCertificatePassword"": ""{password}"",""ClientCertificate"": ""{b64ClientCertificate}"",""AzureCloud"": ""fake-azure-cloud""}}"
+ };
+
+ // Act
+ jobClientBuilderWithFakeBuilder
+ .WithV2CertificateStoreDetails(fakeCertificateStoreDetails)
+ .Build();
+
+ // Assert
+
+ // IAzureGraphClient doesn't require any of the properties set by the builder to be exposed
+ // since the production Build() method creates an Azure Resource Manager client.
+ // But, our builder is fake and exposes the properties we need to test (via the FakeBuilder class).
+ Assert.Equal("fake-tenant-id", jobClientBuilderWithFakeBuilder._builder._tenantId);
+ Assert.Equal("fake-azure-target-object-id", jobClientBuilderWithFakeBuilder._builder._targetObjectId);
Assert.Equal("fake-azure-application-id", jobClientBuilderWithFakeBuilder._builder._applicationId);
Assert.Equal("fake-azure-cloud", jobClientBuilderWithFakeBuilder._builder._azureCloudEndpoint);
Assert.Equal(ssCert.GetCertHash(), jobClientBuilderWithFakeBuilder._builder._clientCertificate!.GetCertHash());
@@ -151,14 +283,14 @@ public static X509Certificate2 GetSelfSignedCert(string hostname)
SubjectAlternativeNameBuilder subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
subjectAlternativeNameBuilder.AddDnsName(hostname);
req.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build());
- req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
+ req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("2.5.29.32.0"), new Oid("1.3.6.1.5.5.7.3.1") }, false));
X509Certificate2 selfSignedCert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
Console.Write($"Created self-signed certificate for \"{hostname}\" with thumbprint {selfSignedCert.Thumbprint}\n");
return selfSignedCert;
}
-
+
static void ConfigureLogging()
{
var config = new NLog.Config.LoggingConfiguration();
@@ -175,7 +307,7 @@ static void ConfigureLogging()
LogHandler.Factory = LoggerFactory.Create(builder =>
{
- builder.AddNLog();
+ builder.AddNLog();
});
}
}
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Discovery.cs b/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Discovery.cs
new file mode 100644
index 0000000..00741c1
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Discovery.cs
@@ -0,0 +1,113 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs;
+
+public class Discovery : IDiscoveryJobExtension
+{
+ public IAzureGraphClient Client { get; set; }
+ public string ExtensionName => "AzureApp2";
+
+ private bool _clientInitializedByInjection = false;
+
+ ILogger _logger = LogHandler.GetClassLogger();
+
+ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate callback)
+ {
+ if (Client != null) _clientInitializedByInjection = true;
+
+ _logger.LogDebug("Beginning Azure Application 2 (App Registration/Application) Discovery Job");
+
+ JobResult result = new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = config.JobHistoryId
+ };
+
+ List discoveredApplicationIds = new();
+
+ foreach (var tenantId in TenantIdsToSearchFromJobConfig(config))
+ {
+ _logger.LogTrace($"Processing tenantId: {tenantId}");
+
+ // If the client was not injected, create a new one with the tenant ID determied by
+ // the TenantIdsToSearchFromJobConfig method
+ if (!_clientInitializedByInjection)
+ {
+ Client = new GraphJobClientBuilder()
+ .WithDiscoveryJobConfiguration(config, tenantId)
+ .Build();
+ }
+
+ try
+ {
+ var operationResult = Client.DiscoverApplicationObjectIds();
+ if (!operationResult.Success)
+ {
+ result.FailureMessage += operationResult.ErrorMessage;
+ _logger.LogWarning(result.FailureMessage);
+ continue;
+ }
+ discoveredApplicationIds.AddRange(operationResult.Result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
+ result.FailureMessage = ex.Message;
+ return result;
+ }
+ }
+
+ try
+ {
+ callback(discoveredApplicationIds);
+ result.Result = OrchestratorJobStatusJobResult.Success;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
+ result.FailureMessage = ex.Message;
+ }
+
+ return result;
+ }
+
+ private IEnumerable TenantIdsToSearchFromJobConfig(DiscoveryJobConfiguration config)
+ {
+ string directoriesToSearchAsString = config.JobProperties?["dirs"] as string;
+ _logger.LogTrace($"Directories to search: {directoriesToSearchAsString}");
+
+ if (string.IsNullOrEmpty(directoriesToSearchAsString) || string.Equals(directoriesToSearchAsString, "*"))
+ {
+ _logger.LogTrace($"No directories to search provided, using default tenant ID: {config.ClientMachine}");
+ return new List { config.ClientMachine };
+ }
+
+ List tenantIdsToSearch = new();
+ tenantIdsToSearch.AddRange(directoriesToSearchAsString.Split(','));
+ tenantIdsToSearch.ForEach(tenantId => tenantId = tenantId.Trim());
+
+ _logger.LogTrace($"Tenant IDs to search: {string.Join(',', tenantIdsToSearch)}");
+ return tenantIdsToSearch;
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Inventory.cs b/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Inventory.cs
new file mode 100644
index 0000000..ced27d8
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Inventory.cs
@@ -0,0 +1,94 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs;
+
+public class Inventory : IInventoryJobExtension
+{
+ public IAzureGraphClient Client { get; set; }
+ public string ExtensionName => "AzureApp2";
+
+ ILogger _logger = LogHandler.GetClassLogger();
+
+ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb)
+ {
+ _logger.LogDebug($"Beginning Azure Application 2 (App Registration/Application) Inventory Job");
+
+ if (Client == null)
+ {
+ Client = new GraphJobClientBuilder()
+ .WithV2CertificateStoreDetails(config.CertificateStoreDetails)
+ .Build();
+ }
+
+ JobResult result = new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = config.JobHistoryId
+ };
+
+ List inventoryItems;
+
+ try
+ {
+ OperationResult> inventoryResult = Client.GetApplicationCertificates();
+ if (!inventoryResult.Success)
+ {
+ // Aggregate the messages into the failure message. Since an exception wasn't thrown,
+ // we still have a partial success. We want to return a warning.
+ result.FailureMessage += inventoryResult.ErrorMessage;
+ result.Result = OrchestratorJobStatusJobResult.Warning;
+ _logger.LogWarning(result.FailureMessage);
+ }
+ else
+ {
+ result.Result = OrchestratorJobStatusJobResult.Success;
+ }
+
+ // At least partial success is guaranteed, so we can continue with the inventory items
+ // that we were able to pull down.
+ inventoryItems = inventoryResult.Result.ToList();
+
+ }
+ catch (Exception ex)
+ {
+
+ // Exception is triggered if we weren't able to pull down the list of certificates
+ // from Azure. This could be due to a number of reasons, including network issues,
+ // or the user not having the correct permissions. An exception won't be triggered
+ // if there are no certificates in the Application, or if we weren't able to assemble
+ // the list of certificates into a CurrentInventoryItem.
+
+ _logger.LogError(ex, "Error getting Application Certificates:\n" + ex.Message);
+ result.FailureMessage = "Error getting Application Certificates:\n" + ex.Message;
+ return result;
+ }
+
+ _logger.LogDebug($"Found {inventoryItems.Count} certificates in Application");
+
+ cb(inventoryItems);
+
+ return result;
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Management.cs b/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Management.cs
new file mode 100644
index 0000000..9ba56f8
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator/AzureApp2Jobs/Management.cs
@@ -0,0 +1,149 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs;
+
+public class Management : IManagementJobExtension
+{
+ public IAzureGraphClient Client { get; set; }
+ public string ExtensionName => "AzureApp";
+
+ ILogger _logger = LogHandler.GetClassLogger();
+
+ public JobResult ProcessJob(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning Application 2 (App Registration/Application) Management Job");
+
+ if (Client == null)
+ {
+ Client = new GraphJobClientBuilder()
+ .WithV2CertificateStoreDetails(config.CertificateStoreDetails)
+ .Build();
+ }
+
+ JobResult result = new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = config.JobHistoryId
+ };
+
+ try
+ {
+ var operation = DetermineOperation(config);
+ result.Result = operation switch
+ {
+ OperationType.Replace => ReplaceCertificate(config),
+ OperationType.Add => AddCertificate(config),
+ OperationType.Remove => RemoveCertificate(config),
+ OperationType.DoNothing => OrchestratorJobStatusJobResult.Success,
+ _ => throw new Exception($"Invalid Management operation type [{config.OperationType}]")
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error processing job: {ex.Message}");
+ result.FailureMessage = ex.Message;
+ }
+
+ return result;
+ }
+
+ private enum OperationType
+ {
+ Add,
+ Remove,
+ Replace,
+ DoNothing,
+ None
+ }
+
+ private OperationType DetermineOperation(ManagementJobConfiguration config)
+ {
+ if (config.OperationType == CertStoreOperationType.Add && config.Overwrite)
+ return OperationType.Replace;
+
+ if (config.OperationType == CertStoreOperationType.Add)
+ return OperationType.Add;
+
+ if (config.OperationType == CertStoreOperationType.Remove)
+ return OperationType.Remove;
+
+ return OperationType.None;
+ }
+
+ private OrchestratorJobStatusJobResult AddCertificate(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning AddCertificate operation");
+
+ // The AzureApp Certificate Store Type doesn't support private key handling
+ if (string.IsNullOrWhiteSpace(config.JobCertificate.PrivateKeyPassword) == false)
+ {
+ throw new Exception("Private key handling is not supported for AzureApp Certificate Store Type.");
+ }
+
+ if (string.IsNullOrWhiteSpace(config.JobCertificate.Alias))
+ {
+ throw new Exception("Certificate alias is required.");
+ }
+
+ _logger.LogTrace($"Adding certificate with alias [{config.JobCertificate.Alias}]");
+
+ // Don't check if the certificate already exists; Command shouldn't allow non-unique
+ // aliases to be added and if the certificate already exists, the operation should fail.
+
+ Client.AddApplicationCertificate(
+ config.JobCertificate.Alias,
+ config.JobCertificate.Contents
+ );
+
+ _logger.LogDebug("AddCertificate operation complete");
+
+ return OrchestratorJobStatusJobResult.Success;
+ }
+
+ private OrchestratorJobStatusJobResult ReplaceCertificate(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning ReplaceCertificate operation");
+
+ RemoveCertificate(config);
+ AddCertificate(config);
+
+ _logger.LogDebug("ReplaceCertificate operation complete");
+
+ return OrchestratorJobStatusJobResult.Success;
+ }
+
+ private OrchestratorJobStatusJobResult RemoveCertificate(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning RemoveCertificate operation");
+
+ _logger.LogTrace($"Removing certificate with alias [{config.JobCertificate.Alias}]");
+
+ // If the certificate doesn't exist, the operation should fail.
+
+ Client.RemoveApplicationCertificate(config.JobCertificate.Alias);
+
+ _logger.LogDebug("RemoveCertificate operation complete");
+
+ return OrchestratorJobStatusJobResult.Success;
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs
index aa3993b..9bcaaf8 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Discovery.cs
@@ -35,6 +35,8 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
{
if (Client != null) _clientInitializedByInjection = true;
+ _logger.LogWarning("Azure Application (App Registration/Application) is DEPRICATED and will be removed in a future version. Please migrate to AzureApp2");
+
_logger.LogDebug("Beginning Azure Application (App Registration/Application) Discovery Job");
JobResult result = new JobResult
@@ -60,7 +62,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
try
{
- var operationResult = Client.DiscoverApplicationObjectIds();
+ var operationResult = Client.DiscoverApplicationApplicationIds();
if (!operationResult.Success)
{
result.FailureMessage += operationResult.ErrorMessage;
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Inventory.cs b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Inventory.cs
index 27cc84c..17ee8f5 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Inventory.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Inventory.cs
@@ -30,21 +30,23 @@ public class Inventory : IInventoryJobExtension
ILogger _logger = LogHandler.GetClassLogger();
- public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb)
+ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb)
{
+ _logger.LogWarning("Azure Application (App Registration/Application) is DEPRICATED and will be removed in a future version. Please migrate to AzureApp2");
+
_logger.LogDebug($"Beginning Azure Application (App Registration/Application) Inventory Job");
if (Client == null)
{
Client = new GraphJobClientBuilder()
- .WithCertificateStoreDetails(config.CertificateStoreDetails)
+ .WithV1CertificateStoreDetails(config.CertificateStoreDetails, ExtensionName)
.Build();
}
JobResult result = new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId
+ JobHistoryId = config.JobHistoryId
};
List inventoryItems;
@@ -56,10 +58,10 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
{
// Aggregate the messages into the failure message. Since an exception wasn't thrown,
// we still have a partial success. We want to return a warning.
- result.FailureMessage += inventoryResult.ErrorMessage;
+ result.FailureMessage += inventoryResult.ErrorMessage;
result.Result = OrchestratorJobStatusJobResult.Warning;
_logger.LogWarning(result.FailureMessage);
- }
+ }
else
{
result.Result = OrchestratorJobStatusJobResult.Success;
@@ -69,7 +71,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
// that we were able to pull down.
inventoryItems = inventoryResult.Result.ToList();
- } catch (Exception ex)
+ }
+ catch (Exception ex)
{
// Exception is triggered if we weren't able to pull down the list of certificates
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Management.cs b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Management.cs
index e210a66..590c077 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Management.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureAppJobs/Management.cs
@@ -30,19 +30,21 @@ public class Management : IManagementJobExtension
public JobResult ProcessJob(ManagementJobConfiguration config)
{
+ _logger.LogWarning("Azure Application (App Registration/Application) is DEPRICATED and will be removed in a future version. Please migrate to AzureApp2");
+
_logger.LogDebug("Beginning Application (App Registration/Application) Management Job");
if (Client == null)
{
Client = new GraphJobClientBuilder()
- .WithCertificateStoreDetails(config.CertificateStoreDetails)
+ .WithV1CertificateStoreDetails(config.CertificateStoreDetails, ExtensionName)
.Build();
}
JobResult result = new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId
+ JobHistoryId = config.JobHistoryId
};
try
@@ -51,10 +53,10 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
result.Result = operation switch
{
OperationType.Replace => ReplaceCertificate(config),
- OperationType.Add => AddCertificate(config),
- OperationType.Remove => RemoveCertificate(config),
- OperationType.DoNothing => OrchestratorJobStatusJobResult.Success,
- _ => throw new Exception($"Invalid Management operation type [{config.OperationType}]")
+ OperationType.Add => AddCertificate(config),
+ OperationType.Remove => RemoveCertificate(config),
+ OperationType.DoNothing => OrchestratorJobStatusJobResult.Success,
+ _ => throw new Exception($"Invalid Management operation type [{config.OperationType}]")
};
}
catch (Exception ex)
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Discovery.cs b/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Discovery.cs
new file mode 100644
index 0000000..accb6df
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Discovery.cs
@@ -0,0 +1,113 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs;
+
+public class Discovery : IDiscoveryJobExtension
+{
+ public IAzureGraphClient Client { get; set; }
+ public string ExtensionName => "AzureSP2";
+
+ private bool _clientInitializedByInjection = false;
+
+ ILogger _logger = LogHandler.GetClassLogger();
+
+ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate callback)
+ {
+ if (Client != null) _clientInitializedByInjection = true;
+
+ _logger.LogDebug("Beginning Azure Service Principal 2 (Enterprise Application/Service Principal) Discovery Job");
+
+ JobResult result = new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = config.JobHistoryId
+ };
+
+ List discoveredApplicationIds = new();
+
+ foreach (var tenantId in TenantIdsToSearchFromJobConfig(config))
+ {
+ _logger.LogTrace($"Processing tenantId: {tenantId}");
+
+ // If the client was not injected, create a new one with the tenant ID determied by
+ // the TenantIdsToSearchFromJobConfig method
+ if (!_clientInitializedByInjection)
+ {
+ Client = new GraphJobClientBuilder()
+ .WithDiscoveryJobConfiguration(config, tenantId)
+ .Build();
+ }
+
+ try
+ {
+ var operationResult = Client.DiscoverServicePrincipalObjectIds();
+ if (!operationResult.Success)
+ {
+ result.FailureMessage += operationResult.ErrorMessage;
+ _logger.LogWarning(result.FailureMessage);
+ continue;
+ }
+ discoveredApplicationIds.AddRange(operationResult.Result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
+ result.FailureMessage = ex.Message;
+ return result;
+ }
+ }
+
+ try
+ {
+ callback(discoveredApplicationIds);
+ result.Result = OrchestratorJobStatusJobResult.Success;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error processing discovery job:\n {ex.Message}");
+ result.FailureMessage = ex.Message;
+ }
+
+ return result;
+ }
+
+ private IEnumerable TenantIdsToSearchFromJobConfig(DiscoveryJobConfiguration config)
+ {
+ string directoriesToSearchAsString = config.JobProperties?["dirs"] as string;
+ _logger.LogTrace($"Directories to search: {directoriesToSearchAsString}");
+
+ if (string.IsNullOrEmpty(directoriesToSearchAsString) || string.Equals(directoriesToSearchAsString, "*"))
+ {
+ _logger.LogTrace($"No directories to search provided, using default tenant ID: {config.ClientMachine}");
+ return new List { config.ClientMachine };
+ }
+
+ List tenantIdsToSearch = new();
+ tenantIdsToSearch.AddRange(directoriesToSearchAsString.Split(','));
+ tenantIdsToSearch.ForEach(tenantId => tenantId = tenantId.Trim());
+
+ _logger.LogTrace($"Tenant IDs to search: {string.Join(',', tenantIdsToSearch)}");
+ return tenantIdsToSearch;
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Inventory.cs b/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Inventory.cs
new file mode 100644
index 0000000..a216fc5
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Inventory.cs
@@ -0,0 +1,94 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs;
+
+public class Inventory : IInventoryJobExtension
+{
+ public IAzureGraphClient Client { get; set; }
+ public string ExtensionName => "AzureSP2";
+
+ ILogger _logger = LogHandler.GetClassLogger();
+
+ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb)
+ {
+ _logger.LogDebug($"Beginning Azure Service Principal 2 (Enterprise Application/Service Principal) Inventory Job");
+
+ if (Client == null)
+ {
+ Client = new GraphJobClientBuilder()
+ .WithV2CertificateStoreDetails(config.CertificateStoreDetails)
+ .Build();
+ }
+
+ JobResult result = new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = config.JobHistoryId
+ };
+
+ List inventoryItems;
+
+ try
+ {
+ OperationResult> inventoryResult = Client.GetServicePrincipalCertificates();
+ if (!inventoryResult.Success)
+ {
+ // Aggregate the messages into the failure message. Since an exception wasn't thrown,
+ // we still have a partial success. We want to return a warning.
+ result.FailureMessage += inventoryResult.ErrorMessage;
+ result.Result = OrchestratorJobStatusJobResult.Warning;
+ _logger.LogWarning(result.FailureMessage);
+ }
+ else
+ {
+ result.Result = OrchestratorJobStatusJobResult.Success;
+ }
+
+ // At least partial success is guaranteed, so we can continue with the inventory items
+ // that we were able to pull down.
+ inventoryItems = inventoryResult.Result.ToList();
+
+ }
+ catch (Exception ex)
+ {
+
+ // Exception is triggered if we weren't able to pull down the list of certificates
+ // from Azure. This could be due to a number of reasons, including network issues,
+ // or the user not having the correct permissions. An exception won't be triggered
+ // if there are no certificates in the Application, or if we weren't able to assemble
+ // the list of certificates into a CurrentInventoryItem.
+
+ _logger.LogError(ex, "Error getting Service Principal (SAML) Certificates:\n" + ex.Message);
+ result.FailureMessage = "Error getting Application Certificates:\n" + ex.Message;
+ return result;
+ }
+
+ _logger.LogDebug($"Found {inventoryItems.Count} certificates in Service Principal (SAML) Application.");
+
+ cb(inventoryItems);
+
+ return result;
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Management.cs b/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Management.cs
new file mode 100644
index 0000000..8ff47de
--- /dev/null
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSP2Jobs/Management.cs
@@ -0,0 +1,151 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using AzureEnterpriseApplicationOrchestrator.Client;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs;
+
+public class Management : IManagementJobExtension
+{
+ public IAzureGraphClient Client { get; set; }
+ public string ExtensionName => "AzureSP2";
+
+ ILogger _logger = LogHandler.GetClassLogger();
+
+ public JobResult ProcessJob(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning Service Principal 2 (Enterprise Application/Service Principal) Management Job");
+
+ if (Client == null)
+ {
+ Client = new GraphJobClientBuilder()
+ .WithV2CertificateStoreDetails(config.CertificateStoreDetails)
+ .Build();
+ }
+
+ JobResult result = new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = config.JobHistoryId
+ };
+
+ try
+ {
+ var operation = DetermineOperation(config);
+ result.Result = operation switch
+ {
+ OperationType.Replace => ReplaceCertificate(config),
+ OperationType.Add => AddCertificate(config),
+ OperationType.Remove => RemoveCertificate(config),
+ OperationType.DoNothing => OrchestratorJobStatusJobResult.Success,
+ _ => throw new Exception($"Invalid Management operation type [{config.OperationType}]")
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error processing job: {ex.Message}");
+ result.FailureMessage = ex.Message;
+ }
+
+ return result;
+ }
+
+ private enum OperationType
+ {
+ Add,
+ Remove,
+ Replace,
+ DoNothing,
+ None
+ }
+
+ private OperationType DetermineOperation(ManagementJobConfiguration config)
+ {
+ if (config.OperationType == CertStoreOperationType.Add && config.Overwrite)
+ return OperationType.Replace;
+
+ if (config.OperationType == CertStoreOperationType.Add)
+ return OperationType.Add;
+
+ if (config.OperationType == CertStoreOperationType.Remove)
+ return OperationType.Remove;
+
+ return OperationType.None;
+ }
+
+ private OrchestratorJobStatusJobResult AddCertificate(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning AddCertificate operation");
+
+ // If a private key password was not provided, Command didn't return
+ // the certificate in PKCS#12 format.
+ if (string.IsNullOrWhiteSpace(config.JobCertificate.PrivateKeyPassword))
+ {
+ throw new Exception("Certificate must be in PKCS#12 format - no private key password provided.");
+ }
+
+ if (string.IsNullOrWhiteSpace(config.JobCertificate.Alias))
+ {
+ throw new Exception("Certificate alias is required.");
+ }
+
+ _logger.LogTrace($"Adding certificate with alias [{config.JobCertificate.Alias}]");
+
+ // Don't check if the certificate already exists; Command shouldn't allow non-unique
+ // aliases to be added and if the certificate already exists, the operation should fail.
+
+ Client.AddServicePrincipalCertificate(
+ config.JobCertificate.Alias,
+ config.JobCertificate.Contents,
+ config.JobCertificate.PrivateKeyPassword
+ );
+
+ _logger.LogDebug("AddCertificate operation complete");
+
+ return OrchestratorJobStatusJobResult.Success;
+ }
+
+ private OrchestratorJobStatusJobResult ReplaceCertificate(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning ReplaceCertificate operation");
+
+ RemoveCertificate(config);
+ AddCertificate(config);
+
+ _logger.LogDebug("ReplaceCertificate operation complete");
+
+ return OrchestratorJobStatusJobResult.Success;
+ }
+
+ private OrchestratorJobStatusJobResult RemoveCertificate(ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Beginning RemoveCertificate operation");
+
+ _logger.LogTrace($"Removing certificate with alias [{config.JobCertificate.Alias}]");
+
+ // If the certificate doesn't exist, the operation should fail.
+
+ Client.RemoveServicePrincipalCertificate(config.JobCertificate.Alias);
+
+ _logger.LogDebug("RemoveCertificate operation complete");
+
+ return OrchestratorJobStatusJobResult.Success;
+ }
+}
+
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs
index 1fa1ee7..599d306 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Discovery.cs
@@ -35,6 +35,8 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
{
if (Client != null) _clientInitializedByInjection = true;
+ _logger.LogWarning("Azure Service Principal (Enterprise Application/Service Principal) is DEPRICATED and will be removed in a future version. Please use AzureSP2");
+
_logger.LogDebug("Beginning Azure Service Principal (Enterprise Application/Service Principal) Discovery Job");
JobResult result = new JobResult
@@ -60,7 +62,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd
try
{
- var operationResult = Client.DiscoverServicePrincipalObjectIds();
+ var operationResult = Client.DiscoverServicePrincipalApplicationIds();
if (!operationResult.Success)
{
result.FailureMessage += operationResult.ErrorMessage;
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Inventory.cs b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Inventory.cs
index 53e7b9e..9279ce5 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Inventory.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Inventory.cs
@@ -30,21 +30,23 @@ public class Inventory : IInventoryJobExtension
ILogger _logger = LogHandler.GetClassLogger();
- public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb)
+ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb)
{
+ _logger.LogWarning("Azure Service Principal (Enterprise Application/Service Principal) is DEPRICATED and will be removed in a future version. Please use AzureSP2");
+
_logger.LogDebug($"Beginning Azure Service Principal (Enterprise Application/Service Principal) Inventory Job");
if (Client == null)
{
Client = new GraphJobClientBuilder()
- .WithCertificateStoreDetails(config.CertificateStoreDetails)
+ .WithV1CertificateStoreDetails(config.CertificateStoreDetails, ExtensionName)
.Build();
}
JobResult result = new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId
+ JobHistoryId = config.JobHistoryId
};
List inventoryItems;
@@ -56,10 +58,10 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
{
// Aggregate the messages into the failure message. Since an exception wasn't thrown,
// we still have a partial success. We want to return a warning.
- result.FailureMessage += inventoryResult.ErrorMessage;
+ result.FailureMessage += inventoryResult.ErrorMessage;
result.Result = OrchestratorJobStatusJobResult.Warning;
_logger.LogWarning(result.FailureMessage);
- }
+ }
else
{
result.Result = OrchestratorJobStatusJobResult.Success;
@@ -69,7 +71,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd
// that we were able to pull down.
inventoryItems = inventoryResult.Result.ToList();
- } catch (Exception ex)
+ }
+ catch (Exception ex)
{
// Exception is triggered if we weren't able to pull down the list of certificates
diff --git a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Management.cs b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Management.cs
index 5540183..5e52827 100644
--- a/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Management.cs
+++ b/AzureEnterpriseApplicationOrchestrator/AzureSPJobs/Management.cs
@@ -30,19 +30,21 @@ public class Management : IManagementJobExtension
public JobResult ProcessJob(ManagementJobConfiguration config)
{
+ _logger.LogWarning("Azure Service Principal (Enterprise Application/Service Principal) is DEPRICATED and will be removed in a future version. Please use AzureSP2");
+
_logger.LogDebug("Beginning Service Principal (Enterprise Application/Service Principal) Management Job");
if (Client == null)
{
Client = new GraphJobClientBuilder()
- .WithCertificateStoreDetails(config.CertificateStoreDetails)
+ .WithV1CertificateStoreDetails(config.CertificateStoreDetails, ExtensionName)
.Build();
}
JobResult result = new JobResult
{
Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId
+ JobHistoryId = config.JobHistoryId
};
try
@@ -51,10 +53,10 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
result.Result = operation switch
{
OperationType.Replace => ReplaceCertificate(config),
- OperationType.Add => AddCertificate(config),
- OperationType.Remove => RemoveCertificate(config),
- OperationType.DoNothing => OrchestratorJobStatusJobResult.Success,
- _ => throw new Exception($"Invalid Management operation type [{config.OperationType}]")
+ OperationType.Add => AddCertificate(config),
+ OperationType.Remove => RemoveCertificate(config),
+ OperationType.DoNothing => OrchestratorJobStatusJobResult.Success,
+ _ => throw new Exception($"Invalid Management operation type [{config.OperationType}]")
};
}
catch (Exception ex)
diff --git a/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs b/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
index 0dfd6c2..467aafc 100644
--- a/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
+++ b/AzureEnterpriseApplicationOrchestrator/Client/GraphClient.cs
@@ -55,6 +55,7 @@ private GraphClient(GraphServiceClient graphClient)
public class Builder : IAzureGraphClientBuilder
{
+ ILogger _logger = LogHandler.GetClassLogger();
private GraphClient _client = new();
private string _tenantId { get; set; }
@@ -62,6 +63,8 @@ public class Builder : IAzureGraphClientBuilder
private string _clientSecret { get; set; }
private X509Certificate2 _clientCertificate { get; set; }
private string _targetObjectId { get; set; }
+ private string _targetServicePrincipalApplicationId { get; set; }
+ private string _targetApplicationApplicationId { get; set; }
private Uri _azureCloudEndpoint { get; set; }
public IAzureGraphClientBuilder WithTenantId(string tenantId)
@@ -76,6 +79,18 @@ public IAzureGraphClientBuilder WithTargetObjectId(string objectId)
return this;
}
+ public IAzureGraphClientBuilder WithTargetServicePrincipalApplicationId(string applicationId)
+ {
+ _targetServicePrincipalApplicationId = applicationId;
+ return this;
+ }
+
+ public IAzureGraphClientBuilder WithTargetApplicationApplicationId(string applicationId)
+ {
+ _targetApplicationApplicationId = applicationId;
+ return this;
+ }
+
public IAzureGraphClientBuilder WithApplicationId(string applicationId)
{
_applicationId = applicationId;
@@ -120,10 +135,60 @@ public IAzureGraphClientBuilder WithAzureCloud(string azureCloud)
return this;
}
+ private string GetApplicationObjectId(GraphServiceClient client, string applicationApplicationId)
+ {
+ ApplicationCollectionResponse apps;
+ try
+ {
+ apps = client.Applications.GetAsync(requestConfiguration =>
+ {
+ requestConfiguration.QueryParameters.Filter = $"(appId eq '{applicationApplicationId}')";
+ requestConfiguration.QueryParameters.Top = 1;
+ }).Result;
+ }
+ catch (AggregateException e)
+ {
+ _logger.LogError($"Unable to query MS Graph for Application \"{applicationApplicationId}\": {e}");
+ throw;
+ }
+
+ if (apps?.Value == null || apps.Value.Count == 0 || string.IsNullOrEmpty(apps.Value.FirstOrDefault()?.Id))
+ {
+ throw new Exception($"Application with Application ID \"{applicationApplicationId}\" not found in tenant \"{_tenantId}\"");
+ }
+
+ return apps.Value.FirstOrDefault()?.Id; ;
+ }
+
+ private string GetServicePrincipalObjectId(GraphServiceClient client, string servicePrincipalApplicationId)
+ {
+ ServicePrincipalCollectionResponse sps;
+ try
+ {
+ sps = client.ServicePrincipals.GetAsync(requestConfiguration =>
+ {
+ requestConfiguration.QueryParameters.Filter = $"(appId eq '{servicePrincipalApplicationId}')";
+ requestConfiguration.QueryParameters.Top = 1;
+ }).Result;
+ }
+ catch (AggregateException e)
+ {
+ _logger.LogError($"Unable to query MS Graph for ServicePrincipal \"{servicePrincipalApplicationId}\": {e}");
+ throw;
+ }
+
+ if (sps?.Value == null || sps.Value.Count == 0 || string.IsNullOrEmpty(sps.Value.FirstOrDefault()?.Id))
+ {
+ throw new Exception($"Service Principal with Application ID \"{servicePrincipalApplicationId}\" not found in tenant \"{_tenantId}\"");
+ }
+
+ return sps.Value.FirstOrDefault()?.Id; ;
+ }
+
public IAzureGraphClient Build()
{
- ILogger logger = LogHandler.GetClassLogger();
- logger.LogDebug($"Creating Graph Client for tenant ID '{_tenantId}' to target application ID '{_applicationId}'.");
+
+ _logger.LogDebug($"Creating Graph Client for tenant ID '{_tenantId}' to target application ID '{_applicationId}'.");
// Setting up credentials for Azure Resource Management.
DefaultAzureCredentialOptions credentialOptions = new DefaultAzureCredentialOptions
@@ -135,15 +200,11 @@ public IAzureGraphClient Build()
TokenCredential credential;
if (!string.IsNullOrWhiteSpace(_clientSecret))
{
- credential = new ClientSecretCredential(
- _tenantId, _applicationId, _clientSecret, credentialOptions
- );
+ credential = new ClientSecretCredential(_tenantId, _applicationId, _clientSecret, credentialOptions);
}
else if (_clientCertificate != null)
{
- credential = new ClientCertificateCredential(
- _tenantId, _applicationId, _clientCertificate, credentialOptions
- );
+ credential = new ClientCertificateCredential(_tenantId, _applicationId, _clientCertificate, credentialOptions);
}
else
{
@@ -155,12 +216,20 @@ public IAzureGraphClient Build()
// Creating Graph Client with the specified credentials.
GraphServiceClient graphClient = new GraphServiceClient(credential, scopes);
+
+ if (string.IsNullOrEmpty(_targetObjectId))
+ {
+ if (!string.IsNullOrEmpty(_targetApplicationApplicationId)) _targetObjectId = GetApplicationObjectId(graphClient, _targetApplicationApplicationId);
+ else if (!string.IsNullOrEmpty(_targetServicePrincipalApplicationId)) _targetObjectId = GetServicePrincipalObjectId(graphClient, _targetServicePrincipalApplicationId);
+ // Discovery job doesn't require a target object ID.
+ }
+
_client._graphClient = graphClient;
_client._credential = credential;
_client._tenantId = _tenantId;
_client._targetObjectId = _targetObjectId;
- logger.LogTrace("Azure Resource Management client created.");
+ _logger.LogTrace("Azure Resource Management client created.");
return _client;
}
}
@@ -189,18 +258,18 @@ public void AddApplicationCertificate(string certificateName, string certificate
_graphClient.Applications[_targetObjectId].PatchAsync(new Application
{
KeyCredentials = new List(DeepCopyKeyList(application.KeyCredentials))
- {
+ {
new KeyCredential {
- DisplayName = certificateName,
- Type = "AsymmetricX509Cert",
- Usage = "Verify",
- CustomKeyIdentifier = customKeyId,
- StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
- EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
- KeyId = Guid.NewGuid(),
- Key = System.Text.Encoding.UTF8.GetBytes(certPem)
- }
+ DisplayName = certificateName,
+ Type = "AsymmetricX509Cert",
+ Usage = "Verify",
+ CustomKeyIdentifier = customKeyId,
+ StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
+ EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
+ KeyId = Guid.NewGuid(),
+ Key = System.Text.Encoding.UTF8.GetBytes(certPem)
}
+ }
}).Wait();
}
catch (AggregateException e)
@@ -282,45 +351,44 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
_graphClient.ServicePrincipals[_targetObjectId].PatchAsync(new ServicePrincipal
{
KeyCredentials = new List()
- {
+ {
new KeyCredential {
- DisplayName = certificateName,
- Type = "AsymmetricX509Cert",
- Usage = "Verify",
- CustomKeyIdentifier = customKeyId,
- StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
- EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
- KeyId = Guid.NewGuid(),
- Key = certificate.Export(X509ContentType.Cert)
+ DisplayName = certificateName,
+ Type = "AsymmetricX509Cert",
+ Usage = "Verify",
+ CustomKeyIdentifier = customKeyId,
+ StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
+ EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
+ KeyId = Guid.NewGuid(),
+ Key = certificate.Export(X509ContentType.Cert)
},
new KeyCredential {
- DisplayName = certificateName,
- Type = "X509CertAndPassword",
- Usage = "Sign",
- CustomKeyIdentifier = customKeyId,
- StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
- EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
- KeyId = privKeyGuid,
- Key = certificate.Export(X509ContentType.Pfx, certificatePassword)
+ DisplayName = certificateName,
+ Type = "X509CertAndPassword",
+ Usage = "Sign",
+ CustomKeyIdentifier = customKeyId,
+ StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
+ EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
+ KeyId = privKeyGuid,
+ Key = certificate.Export(X509ContentType.Pfx, certificatePassword)
}
- },
+ },
PasswordCredentials = new List()
+ {
+ new PasswordCredential
{
- new PasswordCredential
- {
- CustomKeyIdentifier = customKeyId,
- KeyId = privKeyGuid,
- StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
- EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
- SecretText = certificatePassword,
- }
+ CustomKeyIdentifier = customKeyId,
+ KeyId = privKeyGuid,
+ StartDateTime = DateTimeOffset.Parse(certificate.GetEffectiveDateString()),
+ EndDateTime = DateTimeOffset.Parse(certificate.GetExpirationDateString()),
+ SecretText = certificatePassword,
}
+ }
}).Wait();
}
catch (AggregateException e)
{
_logger.LogWarning($"Failed to update service principal object: {e}");
- // TODO remove certificates to avoid leaving the service principal in a bad state
throw;
}
@@ -335,7 +403,6 @@ public void AddServicePrincipalCertificate(string certificateName, string certif
catch (AggregateException e)
{
_logger.LogWarning($"Failed to set preferred SAML certificate: {e}");
- // TODO remove certificates to avoid leaving the service principal in a bad state
throw;
}
}
@@ -443,10 +510,10 @@ public OperationResult> DiscoverApplicationObjectIds()
{
_logger.LogDebug($"Found application \"{app.DisplayName}\" ({app.Id})");
- if (app.Id == null)
+ if (string.IsNullOrEmpty(app.Id))
{
- _logger.LogWarning($"Application \"{app.DisplayName}\" ({app.Id}) does not have an Object ID");
- result.AddRuntimeErrorMessage($"Application \"{app.DisplayName}\" ({app.Id}) does not have an Object ID");
+ _logger.LogWarning($"Application \"{app.DisplayName}\" ({app.AppId}) does not have an Object ID");
+ result.AddRuntimeErrorMessage($"Application \"{app.DisplayName}\" ({app.AppId}) does not have an Object ID");
continue;
}
@@ -482,6 +549,92 @@ public OperationResult> DiscoverServicePrincipalObjectIds()
return result;
}
+ foreach (ServicePrincipal sp in sps.Value)
+ {
+ _logger.LogDebug($"Found SP \"{sp.DisplayName}\" ({sp.Id})");
+
+ if (string.IsNullOrEmpty(sp.Id))
+ {
+ _logger.LogWarning($"Service Principal \"{sp.DisplayName}\" ({sp.Id}) does not have an Object ID");
+ result.AddRuntimeErrorMessage($"Service Principal \"{sp.DisplayName}\" ({sp.Id}) does not have an Object ID");
+ continue;
+ }
+
+ oids.Add($"{sp.Id} ({sp.DisplayName})");
+ }
+
+ return result;
+ }
+
+ public OperationResult> DiscoverApplicationApplicationIds()
+ {
+ List appIds = new();
+ OperationResult> result = new(appIds);
+
+ _logger.LogDebug($"Retrieving application registrations for tenant ID \"{_tenantId}\"");
+ ApplicationCollectionResponse apps;
+ try
+ {
+ apps = _graphClient.Applications.GetAsync((requestConfiguration) =>
+ {
+ requestConfiguration.QueryParameters.Top = 999;
+ }).Result;
+ }
+ catch (AggregateException e)
+ {
+ _logger.LogError($"Unable to retrieve application registrations for tenant ID \"{_tenantId}\": {e}");
+ throw;
+ }
+
+ if (apps?.Value == null || apps.Value.Count == 0)
+ {
+ _logger.LogWarning($"No application registrations found for tenant ID \"{_tenantId}\"");
+ return result;
+ }
+
+ foreach (Application app in apps.Value)
+ {
+ _logger.LogDebug($"Found application \"{app.DisplayName}\" ({app.Id})");
+
+ if (string.IsNullOrEmpty(app.AppId))
+ {
+ _logger.LogWarning($"Application \"{app.DisplayName}\" ({app.Id}) does not have an App ID");
+ result.AddRuntimeErrorMessage($"Application \"{app.DisplayName}\" ({app.Id}) does not have an App ID");
+ continue;
+ }
+
+ appIds.Add($"{app.AppId} ({app.DisplayName})");
+ }
+
+ return result;
+ }
+
+ public OperationResult> DiscoverServicePrincipalApplicationIds()
+ {
+ List appIds = new();
+ OperationResult> result = new(appIds);
+
+ _logger.LogDebug($"Retrieving Service Principals for tenant ID \"{_tenantId}\"");
+ ServicePrincipalCollectionResponse sps;
+ try
+ {
+ sps = _graphClient.ServicePrincipals.GetAsync((requestConfiguration) =>
+ {
+ requestConfiguration.QueryParameters.Top = 999;
+ }).Result;
+ }
+ catch (AggregateException e)
+ {
+ _logger.LogError($"Unable to retrieve Service Principals for tenant ID \"{_tenantId}\": {e}");
+ throw;
+ }
+
+ if (sps?.Value == null || sps.Value.Count == 0)
+ {
+ _logger.LogWarning($"No Service Principals found for tenant ID \"{_tenantId}\"");
+ return result;
+ }
+
foreach (ServicePrincipal sp in sps.Value)
{
_logger.LogDebug($"Found SP \"{sp.DisplayName}\" ({sp.Id})");
@@ -493,7 +646,7 @@ public OperationResult> DiscoverServicePrincipalObjectIds()
continue;
}
- oids.Add($"{sp.Id} ({sp.DisplayName})");
+ appIds.Add($"{sp.AppId} ({sp.DisplayName})");
}
return result;
diff --git a/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs b/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs
index 3f3148a..99be8f0 100644
--- a/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs
+++ b/AzureEnterpriseApplicationOrchestrator/Client/IAzureGraphClient.cs
@@ -20,13 +20,15 @@ namespace AzureEnterpriseApplicationOrchestrator.Client;
public interface IAzureGraphClientBuilder
{
- public IAzureGraphClientBuilder WithTenantId(string tenantId);
- public IAzureGraphClientBuilder WithTargetObjectId(string applicationId);
- public IAzureGraphClientBuilder WithApplicationId(string applicationId);
- public IAzureGraphClientBuilder WithClientSecret(string clientSecret);
- public IAzureGraphClientBuilder WithClientCertificate(X509Certificate2 clientCertificate);
- public IAzureGraphClientBuilder WithAzureCloud(string azureCloud);
- public IAzureGraphClient Build();
+ IAzureGraphClientBuilder WithTenantId(string tenantId);
+ IAzureGraphClientBuilder WithTargetObjectId(string applicationId);
+ IAzureGraphClientBuilder WithTargetServicePrincipalApplicationId(string applicationId);
+ IAzureGraphClientBuilder WithTargetApplicationApplicationId(string applicationId);
+ IAzureGraphClientBuilder WithApplicationId(string applicationId);
+ IAzureGraphClientBuilder WithClientSecret(string clientSecret);
+ IAzureGraphClientBuilder WithClientCertificate(X509Certificate2 clientCertificate);
+ IAzureGraphClientBuilder WithAzureCloud(string azureCloud);
+ IAzureGraphClient Build();
}
public class OperationResult
@@ -65,5 +67,8 @@ public interface IAzureGraphClient
// Discovery
public OperationResult> DiscoverApplicationObjectIds();
+ public OperationResult> DiscoverApplicationApplicationIds();
+
public OperationResult> DiscoverServicePrincipalObjectIds();
+ public OperationResult> DiscoverServicePrincipalApplicationIds();
}
diff --git a/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs b/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs
index 95131f5..d5c8f0d 100644
--- a/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs
+++ b/AzureEnterpriseApplicationOrchestrator/GraphJobClientBuilder.cs
@@ -37,25 +37,44 @@ public record CertificateStoreProperties
public string AzureCloud { get; init; }
}
- public GraphJobClientBuilder WithCertificateStoreDetails(CertificateStore details)
+ public record CertificateStoreV2Properties
{
- _logger.LogDebug($"Builder - Setting values from Certificate Store Details: {JsonConvert.SerializeObject(details)}");
+ public string ServerUsername { get; init; }
+ public string ServerPassword { get; init; }
+ public string ClientCertificate { get; init; }
+ public string ClientCertificatePassword { get; init; }
+ public string AzureCloud { get; init; }
+ }
+
+ public GraphJobClientBuilder WithV1CertificateStoreDetails(CertificateStore details, string storeTypeShortName)
+ {
+ _logger.LogDebug($"Builder - Setting values from V1 Certificate Store Details: {JsonConvert.SerializeObject(details)}");
CertificateStoreProperties properties = JsonConvert.DeserializeObject(details.Properties);
_logger.LogTrace($"Builder - ClientMachine => TenantId: {details.ClientMachine}");
- _logger.LogTrace($"Builder - StorePath => TargetApplicationId: {details.StorePath}");
_logger.LogTrace($"Builder - ServerUsername => ApplicationId: {properties.ServerUsername}");
_logger.LogTrace($"Builder - AzureCloud => AzureCloud: {properties.AzureCloud}");
- // The Discovery Job returns Object IDs in the format ` ()`.
- // We split out the first part to get the Object ID.
- string normalizedObjectID = details.StorePath.Split(" ")[0];
+ // The Discovery Job returns Application IDs in the format ` ()`.
+ // We split out the first part to get the Application ID.
+ string normalizedAppID = details.StorePath.Split(" ")[0];
+
+ if (storeTypeShortName == "AzureApp")
+ {
+ _logger.LogTrace($"Builder - StorePath => TargetApplicationApplicationId: {details.StorePath}");
+ _builder.WithTargetApplicationApplicationId(normalizedAppID);
+ }
+ else if (storeTypeShortName == "AzureSP")
+ {
+ _logger.LogTrace($"Builder - StorePath => TargetServicePrincipalApplicationId: {details.StorePath}");
+ _builder.WithTargetServicePrincipalApplicationId(normalizedAppID);
+ }
+ else throw new Exception($"{storeTypeShortName} is not supported by WithV1CertificateStoreDetails");
_builder
.WithTenantId(details.ClientMachine)
.WithApplicationId(properties.ServerUsername)
- .WithTargetObjectId(normalizedObjectID)
.WithAzureCloud(properties.AzureCloud);
if (string.IsNullOrWhiteSpace(properties.ClientCertificate))
@@ -72,10 +91,53 @@ public GraphJobClientBuilder WithCertificateStoreDetails(CertificateSt
_builder.WithClientCertificate(clientCert);
}
+ return this;
+ }
+
+ public GraphJobClientBuilder WithV2CertificateStoreDetails(CertificateStore details)
+ {
+ _logger.LogDebug($"Builder - Setting values from V2 Certificate Store Details: {JsonConvert.SerializeObject(details)}");
+
+ CertificateStoreV2Properties properties = JsonConvert.DeserializeObject(details.Properties);
+
+ _logger.LogTrace($"Builder - ClientMachine => TenantId: {details.ClientMachine}");
+ _logger.LogTrace($"Builder - StorePath => TargetApplicationObjectId: {details.StorePath}");
+ _logger.LogTrace($"Builder - ServerUsername => ApplicationId: {properties.ServerUsername}");
+ _logger.LogTrace($"Builder - AzureCloud => AzureCloud: {properties.AzureCloud}");
+
+ if (string.IsNullOrEmpty(details.ClientMachine)) throw new Exception("ClientMachine is required");
+ if (string.IsNullOrEmpty(details.StorePath)) throw new Exception("StorePath is required");
+ if (string.IsNullOrEmpty(properties.ServerUsername)) throw new Exception("ServerUsername is required");
+
+ // The Discovery Job returns Object IDs in the format ` ()`.
+ // We split out the first part to get the Object ID.
+ string normalizedObjectID = details.StorePath.Split(" ")[0];
+
+ _builder
+ .WithTenantId(details.ClientMachine)
+ .WithApplicationId(properties.ServerUsername)
+ .WithTargetObjectId(normalizedObjectID)
+ .WithAzureCloud(properties.AzureCloud);
+
+ if (!string.IsNullOrEmpty(properties.ServerPassword))
+ {
+ _logger.LogDebug("Client certificate not present - Using Client Secret authentication");
+ _logger.LogTrace($"Builder - ServerPassword => ClientSecret: {properties.ServerPassword}");
+ _builder.WithClientSecret(properties.ServerPassword);
+ }
+ else if (!string.IsNullOrEmpty(properties.ClientCertificate))
+ {
+ _logger.LogDebug("Client certificate present - Using Client Certificate authentication");
+ _logger.LogTrace($"Builder - ClientCertificatePassword => ClientCertificateKeyPassword: {properties.ClientCertificatePassword}");
+ X509Certificate2 clientCert = SerializeClientCertificate(properties.ClientCertificate, properties.ClientCertificatePassword);
+ _builder.WithClientCertificate(clientCert);
+ }
+ else throw new Exception("One of ClientSecret or ClientCertificate is required to authenticate with Azure Graph");
return this;
}
+
public GraphJobClientBuilder WithDiscoveryJobConfiguration(DiscoveryJobConfiguration config, string tenantId)
{
_logger.LogTrace($"Builder - tenantId => TenantId: {tenantId}");
diff --git a/AzureEnterpriseApplicationOrchestrator/manifest.json b/AzureEnterpriseApplicationOrchestrator/manifest.json
index ff1e8e6..713c60b 100644
--- a/AzureEnterpriseApplicationOrchestrator/manifest.json
+++ b/AzureEnterpriseApplicationOrchestrator/manifest.json
@@ -1,30 +1,54 @@
{
- "extensions": {
- "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": {
- "CertStores.AzureApp.Inventory": {
- "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
- "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureAppJobs.Inventory"
- },
- "CertStores.AzureApp.Management": {
- "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
- "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureAppJobs.Management"
- },
- "CertStores.AzureApp.Discovery": {
- "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
- "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureAppJobs.Discovery"
- },
- "CertStores.AzureSP.Inventory": {
- "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
- "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSPJobs.Inventory"
- },
- "CertStores.AzureSP.Management": {
- "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
- "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSPJobs.Management"
- },
- "CertStores.AzureSP.Discovery": {
- "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
- "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSPJobs.Discovery"
- }
+ "extensions": {
+ "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": {
+ "CertStores.AzureApp.Inventory": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureAppJobs.Inventory"
+ },
+ "CertStores.AzureApp.Management": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureAppJobs.Management"
+ },
+ "CertStores.AzureApp.Discovery": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureAppJobs.Discovery"
+ },
+ "CertStores.AzureSP.Inventory": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSPJobs.Inventory"
+ },
+ "CertStores.AzureSP.Management": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSPJobs.Management"
+ },
+ "CertStores.AzureSP.Discovery": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSPJobs.Discovery"
+ },
+ "CertStores.AzureApp2.Inventory": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs.Inventory"
+ },
+ "CertStores.AzureApp2.Management": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs.Management"
+ },
+ "CertStores.AzureApp2.Discovery": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureApp2Jobs.Discovery"
+ },
+ "CertStores.AzureSP2.Inventory": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs.Inventory"
+ },
+ "CertStores.AzureSP2.Management": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs.Management"
+ },
+ "CertStores.AzureSP2.Discovery": {
+ "assemblypath": "AzureEnterpriseApplicationOrchestrator.dll",
+ "TypeFullName": "AzureEnterpriseApplicationOrchestrator.AzureSP2Jobs.Discovery"
+ }
+ }
}
- }
}
diff --git a/README.md b/README.md
index 0b813e3..cfb883d 100644
--- a/README.md
+++ b/README.md
@@ -29,13 +29,43 @@
-
## Overview
The Azure App Registration and Enterprise Application Orchestrator extension remotely manages both Azure [App Registration/Application](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) certificates and [Enterprise Application/Service Principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/enterprise-apps-certificate-credentials) certificates. Application certificates are typically public key only and used for client certificate authentication, while Service Principal certificates are commonly used for [SAML Assertion signing](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/tutorial-manage-certificates-for-federated-single-sign-on). The extension implements the Inventory, Management Add, Management Remove, and Discovery job types.
Certificates used for client authentication by Applications (configured in App Registrations) are represented by the [`AzureApp` store type](docs/azureapp.md), and certificates used for SSO/SAML assertion signing are represented by the [`AzureSP` store type](docs/azuresp.md). Both store types are managed by the same extension. The extension is configured with a single Azure Service Principal that is used to authenticate to the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). The Azure App Registration and Enterprise Application Orchestrator extension manages certificates for Azure App Registrations (Applications) and Enterprise Applications (Service Principals) differently.
+The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 4 Certificate Store Types. Depending on your use case, you may elect to use one, or all of these Certificate Store Types. Descriptions of each are provided below.
+
+Azure App Registration (Application) (AzureApp)
+
+### AzureApp
+> **WARNING** AzureApp "Azure App Registration (Application)" is **Depricated**. Please use **AzureApp2** "Azure App Registration 2 (Application)" instead.
+
+Azure [App Registration/Application certificates](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) are typically used for client authentication by applications and are typically public key only in Azure. The general model by which these credentials are consumed is that the certificate and private key are accessible by the Application using the App Registration, and are passed to the service that is authenticating the Application. The Azure App Registration and Enterprise Application Orchestrator extension implements the Inventory, Management Add, Management Remove, and Discovery job types for managing these certificates.
+
+
+Azure Enterprise Application (Service Principal) (AzureSP)
+
+### AzureSP
+> **WARNING** AzureSP "Azure Enterprise Application (Service Principal)" is **Depricated**. Please use **AzureSP2** "Azure Enterprise Application 2 (Service Principal)" instead.
+
+The Azure Enterprise Application/Service Principal certificate operations are implemented by the `AzureSP` store type, and supports the management of a single certificate for use in SSO/SAML assertion signing. The Management Add operation is only supported with the certificate replacement option, since adding a new certificate will replace the existing certificate. The Add operation will also set newly added certificates as the active certificate for SSO/SAML usage. The Management Remove operation removes the certificate from the Enterprise Application/Service Principal, which is the same as removing the SSO/SAML signing certificate. The Discovery operation discovers all Enterprise Applications/Service Principals in the tenant.
+
+
+Azure App Registration 2 (Application) (AzureApp2)
+
+### AzureApp2
+Azure [App Registration/Application certificates](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) are typically used for client authentication by applications and are typically public key only in Azure. The general model by which these credentials are consumed is that the certificate and private key are accessible by the Application using the App Registration, and are passed to the service that is authenticating the Application. The Azure App Registration and Enterprise Application Orchestrator extension implements the Inventory, Management Add, Management Remove, and Discovery job types for managing these certificates.
+
+
+Azure Enterprise Application 2 (Service Principal) (AzureSP2)
+
+### AzureSP2
+The Azure Enterprise Application/Service Principal certificate operations are implemented by the `AzureSP` store type, and supports the management of a single certificate for use in SSO/SAML assertion signing. The Management Add operation is only supported with the certificate replacement option, since adding a new certificate will replace the existing certificate. The Add operation will also set newly added certificates as the active certificate for SSO/SAML usage. The Management Remove operation removes the certificate from the Enterprise Application/Service Principal, which is the same as removing the SSO/SAML signing certificate. The Discovery operation discovers all Enterprise Applications/Service Principals in the tenant.
+
+
+
## Compatibility
This integration is compatible with Keyfactor Universal Orchestrator version 10.4 and later.
@@ -45,64 +75,429 @@ The Azure App Registration and Enterprise Application Universal Orchestrator ext
> 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.
-## Installation
+## Requirements & Prerequisites
Before installing the Azure App Registration and Enterprise Application Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command.
-1. **Create Certificate Store Types in Keyfactor Command**
-The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 2 Certificate Store Types. Depending on your use case, you may elect to install one, or all of these Certificate Store Types.
- Azure App Registration (Application)
+### Azure Service Principal (Graph API Authentication)
+
+The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, both Client Secret authentication and Client Certificate authentication (mTLS) are supported. The Service Principal must have the following API Permission:
+- **_Microsoft Graph Application Permissions_**:
+ - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
+
+> For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
+
+Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
+
+#### Client Certificate or Client Secret
+
+Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
+
+* **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+* **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
+
+ The certificate can be in either of the following formats:
+ * Base64-encoded PKCS#12 (PFX) with a matching private key.
+ * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
+
+ If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+
+> **Creating and Formatting a Client Certificate using Keyfactor Command**
+>
+> To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
+>
+> 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
+> 2. Select a **Template** that supports Client Authentication as an extended key usage.
+> 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
+> 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
+> 5. Make a note of the password on the next page - it won't be shown again.
+> 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
+> * If you downloaded the certificate in PEM format, use the commands below:
+>
+> ```shell
+> # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
+> cat
+>
+> # Separate the certificate from the private key
+> openssl x509 -in -out pubkeycert.pem
+>
+> # Base64 encode the certificate and private key
+> cat | base64 > clientcertkeypair.pem.base64
+> ```
+>
+> * If you downloaded the certificate in PFX format, use the commands below:
+>
+> ```shell
+> # Export the certificate from the PFX file
+> openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
+>
+> # Base64 encode the PFX file
+> cat | base64 > clientcert.pfx.base64
+> ```
+> 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
+>
+> You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+
+Azure App Registration (Application) (AzureApp)
+
+### Azure App Registration (Application) Requirements
+
+#### Azure App Registration (Application)
+
+##### Application Certificates
+
+Application certificates are used for client authentication and are typically public key only. No additional configuration in Azure is necessary to manage Application certificates since all App Registrations can contain any number of [Certificates and Secrets](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-credentials). Unless the Discovery job is used, you should collect the Application IDs for each App Registration that contains certificates to be managed.
+
- > More information on the Azure App Registration (Application) Certificate Store Type can be found [here](docs/azureapp.md).
- * **Create AzureApp using kfutil**:
+Azure Enterprise Application (Service Principal) (AzureSP)
+
+### Azure Enterprise Application (Service Principal) Requirements
+
+#### Enterprise Application (Service Principal)
+
+##### Service Principal Certificates
+
+Service Principal certificates are typically used for SAML Token signing. Service Principals are created from Enterprise Applications, and will mostly be configured with a variation of Microsoft's [SAML-based single sign-on](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal) documentation. For more information on the mechanics of the Service Principal certificate management capabilities of this extension, please see the [mechanics](#extension-mechanics) section.
+
+
+
+
+Azure App Registration 2 (Application) (AzureApp2)
+
+### Azure App Registration 2 (Application) Requirements
+
+#### Azure App Registration (Application)
+
+##### Application Certificates
+
+Application certificates are used for client authentication and are typically public key only. No additional configuration in Azure is necessary to manage Application certificates since all App Registrations can contain any number of [Certificates and Secrets](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-credentials). Unless the Discovery job is used, you should collect the Application IDs for each App Registration that contains certificates to be managed.
+
+
+
+
+Azure Enterprise Application 2 (Service Principal) (AzureSP2)
+
+### Azure Enterprise Application 2 (Service Principal) Requirements
+
+#### Enterprise Application (Service Principal)
+
+##### Service Principal Certificates
+
+Service Principal certificates are typically used for SAML Token signing. Service Principals are created from Enterprise Applications, and will mostly be configured with a variation of Microsoft's [SAML-based single sign-on](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal) documentation. For more information on the mechanics of the Service Principal certificate management capabilities of this extension, please see the [mechanics](#extension-mechanics) section.
+
+
+
+
+
+
+## Create Certificate Store Types
+
+To use the Azure App Registration and Enterprise Application Universal Orchestrator extension, you **must** create the Certificate Store Types required for your usecase. This only needs to happen _once_ per Keyfactor Command instance.
+
+The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 4 Certificate Store Types. Depending on your use case, you may elect to use one, or all of these Certificate Store Types.
+
+Azure App Registration (Application) (AzureApp)
+
+
+* **Create AzureApp using kfutil**:
+
+ ```shell
+ # Azure App Registration (Application)
+ kfutil store-types create AzureApp
+ ```
+
+* **Create AzureApp manually in the Command UI**:
+ Create AzureApp manually in the Command UI
+
+ Create a store type called `AzureApp` with the attributes in the tables below:
+
+ #### Basic Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Name | Azure App Registration (Application) | Display name for the store type (may be customized) |
+ | Short Name | AzureApp | Short display name for the store type |
+ | Capability | AzureApp | Store type name orchestrator will register with. Check the box to allow entry of value |
+ | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add |
+ | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove |
+ | Supports Discovery | ✅ Checked | Check the box. Indicates that the Store Type supports Discovery |
+ | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment |
+ | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation |
+ | Needs Server | ✅ Checked | Determines if a target server name is required when creating store |
+ | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint |
+ | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell |
+ | Requires Store Password | 🔲 Unchecked | Enables users to optionally specify a store password when defining a Certificate Store. |
+ | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. |
+
+ The Basic tab should look like this:
+
+ ![AzureApp Basic Tab](docsource/images/AzureApp-basic-store-type-dialog.png)
+
+ #### Advanced Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
+ | Private Key Handling | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
+ | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
+
+ The Advanced tab should look like this:
+
+ ![AzureApp Advanced Tab](docsource/images/AzureApp-advanced-store-type-dialog.png)
+
+ #### Custom Fields Tab
+ Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type:
+
+ | Name | Display Name | Description | Type | Default Value/Options | Required |
+ | ---- | ------------ | ---- | --------------------- | -------- | ----------- |
+ | ServerUsername | Server Username | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. | Secret | | ✅ Checked |
+ | ServerPassword | Server Password | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate. If Client Cert Auth is used _and_ the Client Certificate's private key is not encrypted, you **must** select 'No Value' for this field. | Secret | | 🔲 Unchecked |
+ | ClientCertificate | Client Certificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** select 'No Value' for this field. | Secret | | 🔲 Unchecked |
+ | AzureCloud | Azure Global Cloud Authority Host | Specifies the Azure Cloud instance used by the organization. | MultipleChoice | public,china,germany,government | 🔲 Unchecked |
+ | ServerUseSsl | Use SSL | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | Bool | true | ✅ Checked |
+
+ The Custom Fields tab should look like this:
+
+ ![AzureApp Custom Fields Tab](docsource/images/AzureApp-custom-fields-store-type-dialog.png)
+
- ```shell
- # Azure App Registration (Application)
- kfutil store-types create AzureApp
- ```
- * **Create AzureApp manually in the Command UI**:
-
- Refer to the [Azure App Registration (Application)](docs/azureapp.md#certificate-store-type-configuration) creation docs.
+
- Azure Enterprise Application (Service Principal)
+Azure Enterprise Application (Service Principal) (AzureSP)
- > More information on the Azure Enterprise Application (Service Principal) Certificate Store Type can be found [here](docs/azuresp.md).
+* **Create AzureSP using kfutil**:
+
+ ```shell
+ # Azure Enterprise Application (Service Principal)
+ kfutil store-types create AzureSP
+ ```
+
+* **Create AzureSP manually in the Command UI**:
+ Create AzureSP manually in the Command UI
+
+ Create a store type called `AzureSP` with the attributes in the tables below:
+
+ #### Basic Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Name | Azure Enterprise Application (Service Principal) | Display name for the store type (may be customized) |
+ | Short Name | AzureSP | Short display name for the store type |
+ | Capability | AzureSP | Store type name orchestrator will register with. Check the box to allow entry of value |
+ | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add |
+ | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove |
+ | Supports Discovery | ✅ Checked | Check the box. Indicates that the Store Type supports Discovery |
+ | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment |
+ | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation |
+ | Needs Server | ✅ Checked | Determines if a target server name is required when creating store |
+ | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint |
+ | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell |
+ | Requires Store Password | 🔲 Unchecked | Enables users to optionally specify a store password when defining a Certificate Store. |
+ | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. |
+
+ The Basic tab should look like this:
+
+ ![AzureSP Basic Tab](docsource/images/AzureSP-basic-store-type-dialog.png)
+
+ #### Advanced Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
+ | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
+ | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
+
+ The Advanced tab should look like this:
+
+ ![AzureSP Advanced Tab](docsource/images/AzureSP-advanced-store-type-dialog.png)
+
+ #### Custom Fields Tab
+ Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type:
+
+ | Name | Display Name | Description | Type | Default Value/Options | Required |
+ | ---- | ------------ | ---- | --------------------- | -------- | ----------- |
+ | ServerUsername | Server Username | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. | Secret | | ✅ Checked |
+ | ServerPassword | Server Password | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate. If Client Cert Auth is used _and_ the Client Certificate's private key is not encrypted, you **must** select 'No Value' for this field. | Secret | | 🔲 Unchecked |
+ | ClientCertificate | Client Certificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** select 'No Value' for this field. | Secret | | 🔲 Unchecked |
+ | AzureCloud | Azure Global Cloud Authority Host | Specifies the Azure Cloud instance used by the organization. | MultipleChoice | public,china,germany,government | 🔲 Unchecked |
+ | ServerUseSsl | Use SSL | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | Bool | true | ✅ Checked |
+
+ The Custom Fields tab should look like this:
+
+ ![AzureSP Custom Fields Tab](docsource/images/AzureSP-custom-fields-store-type-dialog.png)
+
+
+
+
+
+
+Azure App Registration 2 (Application) (AzureApp2)
+
+
+* **Create AzureApp2 using kfutil**:
+
+ ```shell
+ # Azure App Registration 2 (Application)
+ kfutil store-types create AzureApp2
+ ```
+
+* **Create AzureApp2 manually in the Command UI**:
+ Create AzureApp2 manually in the Command UI
+
+ Create a store type called `AzureApp2` with the attributes in the tables below:
+
+ #### Basic Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Name | Azure App Registration 2 (Application) | Display name for the store type (may be customized) |
+ | Short Name | AzureApp2 | Short display name for the store type |
+ | Capability | AzureApp2 | Store type name orchestrator will register with. Check the box to allow entry of value |
+ | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add |
+ | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove |
+ | Supports Discovery | ✅ Checked | Check the box. Indicates that the Store Type supports Discovery |
+ | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment |
+ | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation |
+ | Needs Server | ✅ Checked | Determines if a target server name is required when creating store |
+ | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint |
+ | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell |
+ | Requires Store Password | 🔲 Unchecked | Enables users to optionally specify a store password when defining a Certificate Store. |
+ | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. |
+
+ The Basic tab should look like this:
+
+ ![AzureApp2 Basic Tab](docsource/images/AzureApp2-basic-store-type-dialog.png)
+
+ #### Advanced Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
+ | Private Key Handling | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
+ | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
+
+ The Advanced tab should look like this:
+
+ ![AzureApp2 Advanced Tab](docsource/images/AzureApp2-advanced-store-type-dialog.png)
+
+ #### Custom Fields Tab
+ Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type:
+
+ | Name | Display Name | Description | Type | Default Value/Options | Required |
+ | ---- | ------------ | ---- | --------------------- | -------- | ----------- |
+ | ServerUsername | Server Username | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/App Registration certificates. | Secret | | ✅ Checked |
+ | ServerPassword | Server Password | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/App Registration certificates. If Client Certificate Auth is used, you **must** select 'No Value'. | Secret | | 🔲 Unchecked |
+ | ClientCertificate | Client Certificate | The client certificate used to authenticate with Microsoft Graph for managing Application/App Registrations certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** check 'No Value'. | Secret | | 🔲 Unchecked |
+ | ClientCertificatePassword | Client Certificate Password | The (optional) password that encrypts the private key in ClientCertificate. If Client Certificate Auth is not used, you **must** check 'No Value'. | Secret | | 🔲 Unchecked |
+ | AzureCloud | Azure Global Cloud Authority Host | Specifies the Azure Cloud instance used by the organization. | MultipleChoice | public,china,germany,government | 🔲 Unchecked |
+
+ The Custom Fields tab should look like this:
+
+ ![AzureApp2 Custom Fields Tab](docsource/images/AzureApp2-custom-fields-store-type-dialog.png)
+
+
+
+
+
+
+Azure Enterprise Application 2 (Service Principal) (AzureSP2)
+
+
+* **Create AzureSP2 using kfutil**:
+
+ ```shell
+ # Azure Enterprise Application 2 (Service Principal)
+ kfutil store-types create AzureSP2
+ ```
+
+* **Create AzureSP2 manually in the Command UI**:
+ Create AzureSP2 manually in the Command UI
+
+ Create a store type called `AzureSP2` with the attributes in the tables below:
+
+ #### Basic Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Name | Azure Enterprise Application 2 (Service Principal) | Display name for the store type (may be customized) |
+ | Short Name | AzureSP2 | Short display name for the store type |
+ | Capability | AzureSP2 | Store type name orchestrator will register with. Check the box to allow entry of value |
+ | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add |
+ | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove |
+ | Supports Discovery | ✅ Checked | Check the box. Indicates that the Store Type supports Discovery |
+ | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment |
+ | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation |
+ | Needs Server | ✅ Checked | Determines if a target server name is required when creating store |
+ | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint |
+ | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell |
+ | Requires Store Password | 🔲 Unchecked | Enables users to optionally specify a store password when defining a Certificate Store. |
+ | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. |
+
+ The Basic tab should look like this:
+
+ ![AzureSP2 Basic Tab](docsource/images/AzureSP2-basic-store-type-dialog.png)
+
+ #### Advanced Tab
+ | Attribute | Value | Description |
+ | --------- | ----- | ----- |
+ | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
+ | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
+ | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
+
+ The Advanced tab should look like this:
+
+ ![AzureSP2 Advanced Tab](docsource/images/AzureSP2-advanced-store-type-dialog.png)
+
+ #### Custom Fields Tab
+ Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type:
+
+ | Name | Display Name | Description | Type | Default Value/Options | Required |
+ | ---- | ------------ | ---- | --------------------- | -------- | ----------- |
+ | ServerUsername | Server Username | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. | Secret | | ✅ Checked |
+ | ServerPassword | Server Password | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. If Client Certificate Auth is used, you **must** check 'No Value'. | Secret | | 🔲 Unchecked |
+ | ClientCertificate | Client Certificate | The client certificate used to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** check 'No Value'. | Secret | | 🔲 Unchecked |
+ | ClientCertificatePassword | Client Certificate Password | The (optional) password that encrypts the private key in ClientCertificate. If Client Certificate Auth is not used or the certificate's private key is not encrypted, you **must** check 'No Value'. | Secret | | 🔲 Unchecked |
+ | AzureCloud | Azure Global Cloud Authority Host | Specifies the Azure Cloud instance used by the organization. | MultipleChoice | public,china,germany,government | 🔲 Unchecked |
+
+ The Custom Fields tab should look like this:
+
+ ![AzureSP2 Custom Fields Tab](docsource/images/AzureSP2-custom-fields-store-type-dialog.png)
- * **Create AzureSP using kfutil**:
- ```shell
- # Azure Enterprise Application (Service Principal)
- kfutil store-types create AzureSP
- ```
- * **Create AzureSP manually in the Command UI**:
-
- Refer to the [Azure Enterprise Application (Service Principal)](docs/azuresp.md#certificate-store-type-configuration) creation docs.
+
-2. **Download the latest Azure App Registration and Enterprise Application Universal Orchestrator extension from GitHub.**
- On the [Azure App Registration and Enterprise Application Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/azure-application-orchestrator/releases/latest), click the `azure-application-orchestrator` asset to download the zip archive. Unzip the archive containing extension assemblies to a known location.
+## Installation
+
+1. **Download the latest Azure App Registration and Enterprise Application Universal Orchestrator extension from GitHub.**
+
+ Navigate to the [Azure App Registration and Enterprise Application Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/azure-application-orchestrator/releases/latest). Refer to the compatibility matrix below to determine whether the `net6.0` or `net8.0` asset should be downloaded. Then, click the corresponding asset to download the zip archive.
+ | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `azure-application-orchestrator` .NET version to download |
+ | --------- | ----------- | ----------- | ----------- |
+ | Older than `11.0.0` | | | `net6.0` |
+ | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` |
+ | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Never` | `net6.0` |
+ | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` |
+ | `11.6` _and_ newer | `net8.0` | | `net8.0` |
+
+ Unzip the archive containing extension assemblies to a known location.
+
+ > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net6.0`.
-3. **Locate the Universal Orchestrator extensions directory.**
+2. **Locate the Universal Orchestrator extensions directory.**
* **Default on Windows** - `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions`
* **Default on Linux** - `/opt/keyfactor/orchestrator/extensions`
-4. **Create a new directory for the Azure App Registration and Enterprise Application Universal Orchestrator extension inside the extensions directory.**
+3. **Create a new directory for the Azure App Registration and Enterprise Application Universal Orchestrator extension inside the extensions directory.**
Create a new directory called `azure-application-orchestrator`.
> The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory.
-5. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `azure-application-orchestrator` directory.**
+4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `azure-application-orchestrator` directory.**
-6. **Restart the Universal Orchestrator service.**
+5. **Restart the Universal Orchestrator service.**
Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm).
@@ -110,23 +505,374 @@ The Azure App Registration and Enterprise Application Universal Orchestrator ext
> The above installation steps can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions).
-## Configuration and Usage
-The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 2 Certificate Store Types, each of which implements different functionality. Refer to the individual instructions below for each Certificate Store Type that you deemed necessary for your use case from the installation section.
+
+## Defining Certificate Stores
+
+The Azure App Registration and Enterprise Application Universal Orchestrator extension implements 4 Certificate Store Types, each of which implements different functionality. Refer to the individual instructions below for each Certificate Store Type that you deemed necessary for your use case from the installation section.
+
+Azure App Registration (Application) (AzureApp)
+
+
+* **Manually with the Command UI**
+
+ Create Certificate Stores manually in the UI
+
+ 1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.**
+
+ Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_.
+
+ 2. **Add a Certificate Store.**
+
+ Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure App Registration (Application)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID that owns the Service Principal. |
+ | Store Path | The Application ID of the target Application/Service Principal that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureApp` certificates. Specifically, one with the `AzureApp` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate. If Client Cert Auth is used _and_ the Client Certificate's private key is not encrypted, you **must** select 'No Value' for this field. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** select 'No Value' for this field. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+ | ServerUseSsl | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. |
+
+
+
+
+
+
+* **Using kfutil**
+
+ Create Certificate Stores with kfutil
+
+ 1. **Generate a CSV template for the AzureApp certificate store**
+
+ ```shell
+ kfutil stores import generate-template --store-type-name AzureApp --outpath AzureApp.csv
+ ```
+ 2. **Populate the generated CSV file**
+
+ Open the CSV file, and reference the table below to populate parameters for each **Attribute**.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure App Registration (Application)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID that owns the Service Principal. |
+ | Store Path | The Application ID of the target Application/Service Principal that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureApp` certificates. Specifically, one with the `AzureApp` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate. If Client Cert Auth is used _and_ the Client Certificate's private key is not encrypted, you **must** select 'No Value' for this field. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** select 'No Value' for this field. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+ | ServerUseSsl | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. |
+
+
+
+
+ 3. **Import the CSV file to create the certificate stores**
+
+ ```shell
+ kfutil stores import csv --store-type-name AzureApp --file AzureApp.csv
+ ```
+
+
+> The content in this section can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store).
+
+
+
+
+Azure Enterprise Application (Service Principal) (AzureSP)
+
+
+* **Manually with the Command UI**
+
+ Create Certificate Stores manually in the UI
+
+ 1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.**
+
+ Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_.
+
+ 2. **Add a Certificate Store.**
+
+ Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure Enterprise Application (Service Principal)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID that owns the Service Principal. |
+ | Store Path | The Application ID of the target Application/Service Principal that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureSP` certificates. Specifically, one with the `AzureSP` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate. If Client Cert Auth is used _and_ the Client Certificate's private key is not encrypted, you **must** select 'No Value' for this field. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** select 'No Value' for this field. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+ | ServerUseSsl | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. |
+
+
+
+
+
+
+* **Using kfutil**
+
+ Create Certificate Stores with kfutil
+
+ 1. **Generate a CSV template for the AzureSP certificate store**
+
+ ```shell
+ kfutil stores import generate-template --store-type-name AzureSP --outpath AzureSP.csv
+ ```
+ 2. **Populate the generated CSV file**
+
+ Open the CSV file, and reference the table below to populate parameters for each **Attribute**.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure Enterprise Application (Service Principal)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID that owns the Service Principal. |
+ | Store Path | The Application ID of the target Application/Service Principal that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureSP` certificates. Specifically, one with the `AzureSP` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate. If Client Cert Auth is used _and_ the Client Certificate's private key is not encrypted, you **must** select 'No Value' for this field. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** select 'No Value' for this field. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+ | ServerUseSsl | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. |
+
+
+
+
+ 3. **Import the CSV file to create the certificate stores**
+
+ ```shell
+ kfutil stores import csv --store-type-name AzureSP --file AzureSP.csv
+ ```
+
+
+> The content in this section can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store).
+
+
+
+
+Azure App Registration 2 (Application) (AzureApp2)
+
+
+* **Manually with the Command UI**
+
+ Create Certificate Stores manually in the UI
+
+ 1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.**
+
+ Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_.
+
+ 2. **Add a Certificate Store.**
+
+ Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure App Registration 2 (Application)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID where the Application is instantiated |
+ | Store Path | The Object ID of the target Application/App Registration that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureApp2` certificates. Specifically, one with the `AzureApp2` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/App Registration certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/App Registration certificates. If Client Certificate Auth is used, you **must** select 'No Value'. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/App Registrations certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** check 'No Value'. |
+ | ClientCertificatePassword | The (optional) password that encrypts the private key in ClientCertificate. If Client Certificate Auth is not used, you **must** check 'No Value'. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+
+
+
+
+
+
+* **Using kfutil**
+
+ Create Certificate Stores with kfutil
+
+ 1. **Generate a CSV template for the AzureApp2 certificate store**
+
+ ```shell
+ kfutil stores import generate-template --store-type-name AzureApp2 --outpath AzureApp2.csv
+ ```
+ 2. **Populate the generated CSV file**
+
+ Open the CSV file, and reference the table below to populate parameters for each **Attribute**.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure App Registration 2 (Application)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID where the Application is instantiated |
+ | Store Path | The Object ID of the target Application/App Registration that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureApp2` certificates. Specifically, one with the `AzureApp2` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/App Registration certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/App Registration certificates. If Client Certificate Auth is used, you **must** select 'No Value'. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/App Registrations certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** check 'No Value'. |
+ | ClientCertificatePassword | The (optional) password that encrypts the private key in ClientCertificate. If Client Certificate Auth is not used, you **must** check 'No Value'. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+
+
+
+
+ 3. **Import the CSV file to create the certificate stores**
+
+ ```shell
+ kfutil stores import csv --store-type-name AzureApp2 --file AzureApp2.csv
+ ```
+
+
+> The content in this section can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store).
+
+
+
+
+Azure Enterprise Application 2 (Service Principal) (AzureSP2)
+
+
+* **Manually with the Command UI**
+
+ Create Certificate Stores manually in the UI
+
+ 1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.**
+
+ Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_.
+
+ 2. **Add a Certificate Store.**
+
+ Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure Enterprise Application 2 (Service Principal)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID where the Service Principal is instantiated |
+ | Store Path | The Object ID of the target Service Principal/Enterprise Application that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureSP2` certificates. Specifically, one with the `AzureSP2` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. If Client Certificate Auth is used, you **must** check 'No Value'. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** check 'No Value'. |
+ | ClientCertificatePassword | The (optional) password that encrypts the private key in ClientCertificate. If Client Certificate Auth is not used or the certificate's private key is not encrypted, you **must** check 'No Value'. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+
+
+
+
+
+
+* **Using kfutil**
+
+ Create Certificate Stores with kfutil
+
+ 1. **Generate a CSV template for the AzureSP2 certificate store**
+
+ ```shell
+ kfutil stores import generate-template --store-type-name AzureSP2 --outpath AzureSP2.csv
+ ```
+ 2. **Populate the generated CSV file**
+
+ Open the CSV file, and reference the table below to populate parameters for each **Attribute**.
+ | Attribute | Description |
+ | --------- | ----------- |
+ | Category | Select "Azure Enterprise Application 2 (Service Principal)" or the customized certificate store name from the previous step. |
+ | Container | Optional container to associate certificate store with. |
+ | Client Machine | The Azure Tenant (directory) ID where the Service Principal is instantiated |
+ | Store Path | The Object ID of the target Service Principal/Enterprise Application that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. |
+ | Orchestrator | Select an approved orchestrator capable of managing `AzureSP2` certificates. Specifically, one with the `AzureSP2` capability. |
+ | ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. |
+ | ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. If Client Certificate Auth is used, you **must** check 'No Value'. |
+ | ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Service Principal/Enterprise Application certificates. See the [requirements](#client-certificate-or-client-secret) for more information. If Client Certificate Auth is not used, you **must** check 'No Value'. |
+ | ClientCertificatePassword | The (optional) password that encrypts the private key in ClientCertificate. If Client Certificate Auth is not used or the certificate's private key is not encrypted, you **must** check 'No Value'. |
+ | AzureCloud | Specifies the Azure Cloud instance used by the organization. |
+
+
+
+
+ 3. **Import the CSV file to create the certificate stores**
+
+ ```shell
+ kfutil stores import csv --store-type-name AzureSP2 --file AzureSP2.csv
+ ```
+
+
+> The content in this section can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store).
+
+
+
+
+## Discovering Certificate Stores with the Discovery Job
+> The Discovery Job for all four Certificate Store Types implemented by the Azure App Registration and Enterprise Application Orchestrator extension returns Store Paths in the format ` ()`. When defining Certificate Stores manually, you may elect to follow this format, or use the standard `` for the Store Path.
Azure App Registration (Application)
-1. Refer to the [requirements section](docs/azureapp.md#requirements) to ensure all prerequisites are met before using the Azure App Registration (Application) Certificate Store Type.
-2. Create new [Azure App Registration (Application)](docs/azureapp.md#certificate-store-configuration) Certificate Stores in Keyfactor Command.
+
+### Azure App Registration (Application) Discovery Job
+
+The Discovery operation discovers all Azure App Registrations that the Service Principal has access to. The discovered App Registrations (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
+
+The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
+- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure App Registrations that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
+- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure App Registrations available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
+
+> The Discovery Job only supports Client Secret authentication.
+
Azure Enterprise Application (Service Principal)
-1. Refer to the [requirements section](docs/azuresp.md#requirements) to ensure all prerequisites are met before using the Azure Enterprise Application (Service Principal) Certificate Store Type.
-2. Create new [Azure Enterprise Application (Service Principal)](docs/azuresp.md#certificate-store-configuration) Certificate Stores in Keyfactor Command.
+
+### Azure Enterprise Application (Service Principal) Discovery Job
+
+The Discovery operation discovers all Azure Enterprise Applications that the Service Principal has access to. The discovered Enterprise Applications (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
+
+The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
+- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure Enterprise Applications that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
+- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure Enterprise Applications available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
+
+> The Discovery Job only supports Client Secret authentication.
+
+
+
+Azure App Registration 2 (Application)
+
+
+### Azure App Registration 2 (Application) Discovery Job
+
+The Discovery operation discovers all Azure App Registrations that the Service Principal has access to. The discovered App Registrations (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
+
+The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
+- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure App Registrations that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
+- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure App Registrations available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
+
+> The Discovery Job only supports Client Secret authentication.
+
+
+
+Azure Enterprise Application 2 (Service Principal)
+
+
+### Azure Enterprise Application 2 (Service Principal) Discovery Job
+
+The Discovery operation discovers all Azure Enterprise Applications that the Service Principal has access to. The discovered Enterprise Applications (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
+
+The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
+- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure Enterprise Applications that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
+- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure Enterprise Applications available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
+
+> The Discovery Job only supports Client Secret authentication.
+
+
+## Extension Mechanics
+
+The Azure App Registration and Enterprise Application Orchestrator extension uses the [Microsoft Dotnet Graph SDK](https://learn.microsoft.com/en-us/graph/sdks/sdks-overview) to interact with the Microsoft Graph API. The extension uses the following Graph API endpoints to manage Application certificates:
+
+* [Get Application](https://learn.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http) - Used to obtain the Object ID of the App Registration, and to download the certificates owned by the App Registration.
+* [Update Application](https://learn.microsoft.com/en-us/graph/api/application-update?view=graph-rest-1.0&tabs=http) - Used to modify the App Registration to add or remove certificates.
+ * Specifically, the extension manipulates the [`keyCredentials` resource](https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) of the Application object.
+
+
## License
Apache License 2.0, see [LICENSE](LICENSE).
diff --git a/docs/azureapp.md b/docs/azureapp.md
deleted file mode 100644
index 8b578ed..0000000
--- a/docs/azureapp.md
+++ /dev/null
@@ -1,206 +0,0 @@
-## Azure App Registration (Application)
-
-Azure [App Registration/Application certificates](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) are typically used for client authentication by applications and are typically public key only in Azure. The general model by which these credentials are consumed is that the certificate and private key are accessible by the Application using the App Registration, and are passed to the service that is authenticating the Application. The Azure App Registration and Enterprise Application Orchestrator extension implements the Inventory, Management Add, Management Remove, and Discovery job types for managing these certificates.
-
-
-
-### Supported Job Types
-
-| Job Name | Supported |
-| -------- | --------- |
-| Inventory | ✅ |
-| Management Add | ✅ |
-| Management Remove | ✅ |
-| Discovery | ✅ |
-| Create | |
-| Reenrollment | |
-
-## Requirements
-
-#### Azure Service Principal (Graph API Authentication)
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, Client Secret authentication is supported. The Service Principal must have the following API Permission:
-- **_Microsoft Graph Application Permissions_**:
- - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
-
-> For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
-
-Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
-
-##### Client Certificate or Client Secret
-
-Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
-
-* **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-* **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
-
- The certificate can be in either of the following formats:
- * Base64-encoded PKCS#12 (PFX) with a matching private key.
- * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
-
- If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-> **Creating and Formatting a Client Certificate using Keyfactor Command**
->
-> To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
->
-> 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
-> 2. Select a **Template** that supports Client Authentication as an extended key usage.
-> 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
-> 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
-> 5. Make a note of the password on the next page - it won't be shown again.
-> 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
-> * If you downloaded the certificate in PEM format, use the commands below:
->
-> ```shell
-> # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
-> cat
->
-> # Separate the certificate from the private key
-> openssl x509 -in -out pubkeycert.pem
->
-> # Base64 encode the certificate and private key
-> cat | base64 > clientcertkeypair.pem.base64
-> ```
->
-> * If you downloaded the certificate in PFX format, use the commands below:
->
-> ```shell
-> # Export the certificate from the PFX file
-> openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
->
-> # Base64 encode the PFX file
-> cat | base64 > clientcert.pfx.base64
-> ```
-> 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
->
-> You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-#### Azure App Registration (Application)
-
-##### Application Certificates
-
-Application certificates are used for client authentication and are typically public key only. No additional configuration in Azure is necessary to manage Application certificates since all App Registrations can contain any number of [Certificates and Secrets](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-credentials). Unless the Discovery job is used, you should collect the Application IDs for each App Registration that contains certificates to be managed.
-
-
-## Certificate Store Type Configuration
-
-The recommended method for creating the `AzureApp` Certificate Store Type is to use [kfutil](https://github.com/Keyfactor/kfutil). After installing, use the following command to create the `AzureApp` Certificate Store Type:
-
-```shell
-kfutil store-types create AzureApp
-```
-
-AzureApp
-
-Create a store type called `AzureApp` with the attributes in the tables below:
-
-### Basic Tab
-| Attribute | Value | Description |
-| --------- | ----- | ----- |
-| Name | Azure App Registration (Application) | Display name for the store type (may be customized) |
-| Short Name | AzureApp | Short display name for the store type |
-| Capability | AzureApp | Store type name orchestrator will register with. Check the box to allow entry of value |
-| Supported Job Types (check the box for each) | Add, Discovery, Remove | Job types the extension supports |
-| Supports Add | ✅ | Check the box. Indicates that the Store Type supports Management Add |
-| Supports Remove | ✅ | Check the box. Indicates that the Store Type supports Management Remove |
-| Supports Discovery | ✅ | Check the box. Indicates that the Store Type supports Discovery |
-| Supports Reenrollment | | Indicates that the Store Type supports Reenrollment |
-| Supports Create | | Indicates that the Store Type supports store creation |
-| Needs Server | ✅ | Determines if a target server name is required when creating store |
-| Blueprint Allowed | | Determines if store type may be included in an Orchestrator blueprint |
-| Uses PowerShell | | Determines if underlying implementation is PowerShell |
-| Requires Store Password | | Determines if a store password is required when configuring an individual store. |
-| Supports Entry Password | | Determines if an individual entry within a store can have a password. |
-
-The Basic tab should look like this:
-
-![AzureApp Basic Tab](../docsource/images/AzureApp-basic-store-type-dialog.png)
-
-### Advanced Tab
-| Attribute | Value | Description |
-| --------- | ----- | ----- |
-| Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
-| Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
-| PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
-
-The Advanced tab should look like this:
-
-![AzureApp Advanced Tab](../docsource/images/AzureApp-advanced-store-type-dialog.png)
-
-### Custom Fields Tab
-Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type:
-
-| Name | Display Name | Type | Default Value/Options | Required | Description |
-| ---- | ------------ | ---- | --------------------- | -------- | ----------- |
-| ServerUsername | Server Username | Secret | | | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. |
-| ServerPassword | Server Password | Secret | | | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate |
-| ClientCertificate | Client Certificate | Secret | | | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. |
-| AzureCloud | Azure Global Cloud Authority Host | MultipleChoice | public,china,germany,government | | Specifies the Azure Cloud instance used by the organization. |
-| ServerUseSsl | Use SSL | Bool | true | ✅ | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. |
-
-
-The Custom Fields tab should look like this:
-
-![AzureApp Custom Fields Tab](../docsource/images/AzureApp-custom-fields-store-type-dialog.png)
-
-
-
-
-
-
-## Extension Mechanics
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses the [Microsoft Dotnet Graph SDK](https://learn.microsoft.com/en-us/graph/sdks/sdks-overview) to interact with the Microsoft Graph API. The extension uses the following Graph API endpoints to manage Application certificates:
-
-* [Get Application](https://learn.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http) - Used to obtain the Object ID of the App Registration, and to download the certificates owned by the App Registration.
-* [Update Application](https://learn.microsoft.com/en-us/graph/api/application-update?view=graph-rest-1.0&tabs=http) - Used to modify the App Registration to add or remove certificates.
- * Specifically, the extension manipulates the [`keyCredentials` resource](https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) of the Application object.
-
-#### Discovery Job
-
-The Discovery operation discovers all Azure App Registrations that the Service Principal has access to. The discovered App Registrations (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
-
-The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
-- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure App Registrations that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
-- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure App Registrations available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
-
-> The Discovery Job only supports Client Secret authentication.
-
-
-
-
-
-## Certificate Store Configuration
-
-After creating the `AzureApp` Certificate Store Type and installing the Azure App Registration and Enterprise Application Universal Orchestrator extension, you can create new [Certificate Stores](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store) to manage certificates in the remote platform.
-
-The following table describes the required and optional fields for the `AzureApp` certificate store type.
-
-| Attribute | Description | Attribute is PAM Eligible |
-| --------- | ----------- | ------------------------- |
-| Category | Select "Azure App Registration (Application)" or the customized certificate store name from the previous step. | |
-| Container | Optional container to associate certificate store with. | |
-| Client Machine | The Azure Tenant (directory) ID that owns the Service Principal. | |
-| Store Path | The Application ID of the target Application/Service Principal that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. | |
-| Orchestrator | Select an approved orchestrator capable of managing `AzureApp` certificates. Specifically, one with the `AzureApp` capability. | |
-| ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. | |
-| ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate | |
-| ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. | |
-| AzureCloud | Specifies the Azure Cloud instance used by the organization. | |
-| ServerUseSsl | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | |
-
-* **Using kfutil**
-
- ```shell
- # Generate a CSV template for the AzureApp certificate store
- kfutil stores import generate-template --store-type-name AzureApp --outpath AzureApp.csv
-
- # Open the CSV file and fill in the required fields for each certificate store.
-
- # Import the CSV file to create the certificate stores
- kfutil stores import csv --store-type-name AzureApp --file AzureApp.csv
- ```
-
-* **Manually with the Command UI**: In Keyfactor Command, navigate to Certificate Stores from the Locations Menu. Click the Add button to create a new Certificate Store using the attributes in the table above.
-
diff --git a/docs/azuresp.md b/docs/azuresp.md
deleted file mode 100644
index 1029f86..0000000
--- a/docs/azuresp.md
+++ /dev/null
@@ -1,206 +0,0 @@
-## Azure Enterprise Application (Service Principal)
-
-The Azure Enterprise Application/Service Principal certificate operations are implemented by the `AzureSP` store type, and supports the management of a single certificate for use in SSO/SAML assertion signing. The Management Add operation is only supported with the certificate replacement option, since adding a new certificate will replace the existing certificate. The Add operation will also set newly added certificates as the active certificate for SSO/SAML usage. The Management Remove operation removes the certificate from the Enterprise Application/Service Principal, which is the same as removing the SSO/SAML signing certificate. The Discovery operation discovers all Enterprise Applications/Service Principals in the tenant.
-
-
-
-### Supported Job Types
-
-| Job Name | Supported |
-| -------- | --------- |
-| Inventory | ✅ |
-| Management Add | ✅ |
-| Management Remove | ✅ |
-| Discovery | ✅ |
-| Create | |
-| Reenrollment | |
-
-## Requirements
-
-#### Azure Service Principal (Graph API Authentication)
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, Client Secret authentication is supported. The Service Principal must have the following API Permission:
-- **_Microsoft Graph Application Permissions_**:
- - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
-
-> For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
-
-Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
-
-##### Client Certificate or Client Secret
-
-Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
-
-* **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-* **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
-
- The certificate can be in either of the following formats:
- * Base64-encoded PKCS#12 (PFX) with a matching private key.
- * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
-
- If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-> **Creating and Formatting a Client Certificate using Keyfactor Command**
->
-> To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
->
-> 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
-> 2. Select a **Template** that supports Client Authentication as an extended key usage.
-> 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
-> 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
-> 5. Make a note of the password on the next page - it won't be shown again.
-> 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
-> * If you downloaded the certificate in PEM format, use the commands below:
->
-> ```shell
-> # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
-> cat
->
-> # Separate the certificate from the private key
-> openssl x509 -in -out pubkeycert.pem
->
-> # Base64 encode the certificate and private key
-> cat | base64 > clientcertkeypair.pem.base64
-> ```
->
-> * If you downloaded the certificate in PFX format, use the commands below:
->
-> ```shell
-> # Export the certificate from the PFX file
-> openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
->
-> # Base64 encode the PFX file
-> cat | base64 > clientcert.pfx.base64
-> ```
-> 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
->
-> You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-#### Enterprise Application (Service Principal)
-
-##### Service Principal Certificates
-
-Service Principal certificates are typically used for SAML Token signing. Service Principals are created from Enterprise Applications, and will mostly be configured with a variation of Microsoft's [SAML-based single sign-on](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal) documentation. For more information on the mechanics of the Service Principal certificate management capabilities of this extension, please see the [mechanics](#extension-mechanics) section.
-
-
-## Certificate Store Type Configuration
-
-The recommended method for creating the `AzureSP` Certificate Store Type is to use [kfutil](https://github.com/Keyfactor/kfutil). After installing, use the following command to create the `AzureSP` Certificate Store Type:
-
-```shell
-kfutil store-types create AzureSP
-```
-
-AzureSP
-
-Create a store type called `AzureSP` with the attributes in the tables below:
-
-### Basic Tab
-| Attribute | Value | Description |
-| --------- | ----- | ----- |
-| Name | Azure Enterprise Application (Service Principal) | Display name for the store type (may be customized) |
-| Short Name | AzureSP | Short display name for the store type |
-| Capability | AzureSP | Store type name orchestrator will register with. Check the box to allow entry of value |
-| Supported Job Types (check the box for each) | Add, Discovery, Remove | Job types the extension supports |
-| Supports Add | ✅ | Check the box. Indicates that the Store Type supports Management Add |
-| Supports Remove | ✅ | Check the box. Indicates that the Store Type supports Management Remove |
-| Supports Discovery | ✅ | Check the box. Indicates that the Store Type supports Discovery |
-| Supports Reenrollment | | Indicates that the Store Type supports Reenrollment |
-| Supports Create | | Indicates that the Store Type supports store creation |
-| Needs Server | ✅ | Determines if a target server name is required when creating store |
-| Blueprint Allowed | | Determines if store type may be included in an Orchestrator blueprint |
-| Uses PowerShell | | Determines if underlying implementation is PowerShell |
-| Requires Store Password | | Determines if a store password is required when configuring an individual store. |
-| Supports Entry Password | | Determines if an individual entry within a store can have a password. |
-
-The Basic tab should look like this:
-
-![AzureSP Basic Tab](../docsource/images/AzureSP-basic-store-type-dialog.png)
-
-### Advanced Tab
-| Attribute | Value | Description |
-| --------- | ----- | ----- |
-| Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
-| Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
-| PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |
-
-The Advanced tab should look like this:
-
-![AzureSP Advanced Tab](../docsource/images/AzureSP-advanced-store-type-dialog.png)
-
-### Custom Fields Tab
-Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type:
-
-| Name | Display Name | Type | Default Value/Options | Required | Description |
-| ---- | ------------ | ---- | --------------------- | -------- | ----------- |
-| ServerUsername | Server Username | Secret | | | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. |
-| ServerPassword | Server Password | Secret | | | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate |
-| ClientCertificate | Client Certificate | Secret | | | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. |
-| AzureCloud | Azure Global Cloud Authority Host | MultipleChoice | public,china,germany,government | | Specifies the Azure Cloud instance used by the organization. |
-| ServerUseSsl | Use SSL | Bool | true | ✅ | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. |
-
-
-The Custom Fields tab should look like this:
-
-![AzureSP Custom Fields Tab](../docsource/images/AzureSP-custom-fields-store-type-dialog.png)
-
-
-
-
-
-
-## Extension Mechanics
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses the [Microsoft Dotnet Graph SDK](https://learn.microsoft.com/en-us/graph/sdks/sdks-overview) to interact with the Microsoft Graph API. The extension uses the following Graph API endpoints to manage Service Principal certificates:
-
-* [Get Service Principal](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0&tabs=http) - Used to obtain the Object ID of the Enterprise Application, and to download the certificates owned by the Service Principal.
-* [Update Service Principal](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-update?view=graph-rest-1.0&tabs=http) - Used to modify the Enterprise Application to add or remove certificates.
- * Specifically, the extension manipulates the [`keyCredentials` resource](https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) of the Service Principal object.
-
-#### Discovery Job
-
-The Discovery operation discovers all Azure Enterprise Applications that the Service Principal has access to. The discovered Enterprise Applications (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
-
-The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
-- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure Enterprise Applications that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
-- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure Enterprise Applications available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
-
-> The Discovery Job only supports Client Secret authentication.
-
-
-
-
-
-## Certificate Store Configuration
-
-After creating the `AzureSP` Certificate Store Type and installing the Azure App Registration and Enterprise Application Universal Orchestrator extension, you can create new [Certificate Stores](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store) to manage certificates in the remote platform.
-
-The following table describes the required and optional fields for the `AzureSP` certificate store type.
-
-| Attribute | Description | Attribute is PAM Eligible |
-| --------- | ----------- | ------------------------- |
-| Category | Select "Azure Enterprise Application (Service Principal)" or the customized certificate store name from the previous step. | |
-| Container | Optional container to associate certificate store with. | |
-| Client Machine | The Azure Tenant (directory) ID that owns the Service Principal. | |
-| Store Path | The Application ID of the target Application/Service Principal that will be managed by the Azure App Registration and Enterprise Application Orchestrator extension. | |
-| Orchestrator | Select an approved orchestrator capable of managing `AzureSP` certificates. Specifically, one with the `AzureSP` capability. | |
-| ServerUsername | The Application ID of the Service Principal used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. | |
-| ServerPassword | A Client Secret that the extension will use to authenticate with Microsoft Graph for managing Application/Service Principal certificates, OR the password that encrypts the private key in ClientCertificate | |
-| ClientCertificate | The client certificate used to authenticate with Microsoft Graph for managing Application/Service Principal certificates. See the [requirements](#client-certificate-or-client-secret) for more information. | |
-| AzureCloud | Specifies the Azure Cloud instance used by the organization. | |
-| ServerUseSsl | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | |
-
-* **Using kfutil**
-
- ```shell
- # Generate a CSV template for the AzureApp certificate store
- kfutil stores import generate-template --store-type-name AzureSP --outpath AzureSP.csv
-
- # Open the CSV file and fill in the required fields for each certificate store.
-
- # Import the CSV file to create the certificate stores
- kfutil stores import csv --store-type-name AzureSP --file AzureSP.csv
- ```
-
-* **Manually with the Command UI**: In Keyfactor Command, navigate to Certificate Stores from the Locations Menu. Click the Add button to create a new Certificate Store using the attributes in the table above.
-
diff --git a/docsource/azureapp.md b/docsource/azureapp.md
index 457bb74..ad8e821 100644
--- a/docsource/azureapp.md
+++ b/docsource/azureapp.md
@@ -1,84 +1,18 @@
# Overview
+> **WARNING** AzureApp "Azure App Registration (Application)" is **Depricated**. Please use **AzureApp2** "Azure App Registration 2 (Application)" instead.
+
Azure [App Registration/Application certificates](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) are typically used for client authentication by applications and are typically public key only in Azure. The general model by which these credentials are consumed is that the certificate and private key are accessible by the Application using the App Registration, and are passed to the service that is authenticating the Application. The Azure App Registration and Enterprise Application Orchestrator extension implements the Inventory, Management Add, Management Remove, and Discovery job types for managing these certificates.
# Requirements
-### Azure Service Principal (Graph API Authentication)
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, Client Secret authentication is supported. The Service Principal must have the following API Permission:
-- **_Microsoft Graph Application Permissions_**:
- - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
-
-> For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
-
-Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
-
-#### Client Certificate or Client Secret
-
-Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
-
-* **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-* **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
-
- The certificate can be in either of the following formats:
- * Base64-encoded PKCS#12 (PFX) with a matching private key.
- * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
+## Azure App Registration (Application)
- If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-> **Creating and Formatting a Client Certificate using Keyfactor Command**
->
-> To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
->
-> 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
-> 2. Select a **Template** that supports Client Authentication as an extended key usage.
-> 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
-> 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
-> 5. Make a note of the password on the next page - it won't be shown again.
-> 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
-> * If you downloaded the certificate in PEM format, use the commands below:
->
-> ```shell
-> # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
-> cat
->
-> # Separate the certificate from the private key
-> openssl x509 -in -out pubkeycert.pem
->
-> # Base64 encode the certificate and private key
-> cat | base64 > clientcertkeypair.pem.base64
-> ```
->
-> * If you downloaded the certificate in PFX format, use the commands below:
->
-> ```shell
-> # Export the certificate from the PFX file
-> openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
->
-> # Base64 encode the PFX file
-> cat | base64 > clientcert.pfx.base64
-> ```
-> 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
->
-> You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-
-### Azure App Registration (Application)
-
-#### Application Certificates
+### Application Certificates
Application certificates are used for client authentication and are typically public key only. No additional configuration in Azure is necessary to manage Application certificates since all App Registrations can contain any number of [Certificates and Secrets](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-credentials). Unless the Discovery job is used, you should collect the Application IDs for each App Registration that contains certificates to be managed.
-# Extension Mechanics
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses the [Microsoft Dotnet Graph SDK](https://learn.microsoft.com/en-us/graph/sdks/sdks-overview) to interact with the Microsoft Graph API. The extension uses the following Graph API endpoints to manage Application certificates:
-
-* [Get Application](https://learn.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http) - Used to obtain the Object ID of the App Registration, and to download the certificates owned by the App Registration.
-* [Update Application](https://learn.microsoft.com/en-us/graph/api/application-update?view=graph-rest-1.0&tabs=http) - Used to modify the App Registration to add or remove certificates.
- * Specifically, the extension manipulates the [`keyCredentials` resource](https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) of the Application object.
-
-### Discovery Job
+# Discovery Job Configuration
The Discovery operation discovers all Azure App Registrations that the Service Principal has access to. The discovered App Registrations (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
diff --git a/docsource/azureapp2.md b/docsource/azureapp2.md
new file mode 100644
index 0000000..ede81a5
--- /dev/null
+++ b/docsource/azureapp2.md
@@ -0,0 +1,21 @@
+# Overview
+
+Azure [App Registration/Application certificates](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) are typically used for client authentication by applications and are typically public key only in Azure. The general model by which these credentials are consumed is that the certificate and private key are accessible by the Application using the App Registration, and are passed to the service that is authenticating the Application. The Azure App Registration and Enterprise Application Orchestrator extension implements the Inventory, Management Add, Management Remove, and Discovery job types for managing these certificates.
+
+# Requirements
+
+## Azure App Registration (Application)
+
+### Application Certificates
+
+Application certificates are used for client authentication and are typically public key only. No additional configuration in Azure is necessary to manage Application certificates since all App Registrations can contain any number of [Certificates and Secrets](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-credentials). Unless the Discovery job is used, you should collect the Application IDs for each App Registration that contains certificates to be managed.
+
+# Discovery Job Configuration
+
+The Discovery operation discovers all Azure App Registrations that the Service Principal has access to. The discovered App Registrations (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
+
+The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
+- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure App Registrations that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
+- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure App Registrations available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
+
+> The Discovery Job only supports Client Secret authentication.
diff --git a/docsource/azuresp.md b/docsource/azuresp.md
index 864b84a..692942c 100644
--- a/docsource/azuresp.md
+++ b/docsource/azuresp.md
@@ -1,83 +1,18 @@
# Overview
+> **WARNING** AzureSP "Azure Enterprise Application (Service Principal)" is **Depricated**. Please use **AzureSP2** "Azure Enterprise Application 2 (Service Principal)" instead.
+
The Azure Enterprise Application/Service Principal certificate operations are implemented by the `AzureSP` store type, and supports the management of a single certificate for use in SSO/SAML assertion signing. The Management Add operation is only supported with the certificate replacement option, since adding a new certificate will replace the existing certificate. The Add operation will also set newly added certificates as the active certificate for SSO/SAML usage. The Management Remove operation removes the certificate from the Enterprise Application/Service Principal, which is the same as removing the SSO/SAML signing certificate. The Discovery operation discovers all Enterprise Applications/Service Principals in the tenant.
# Requirements
-### Azure Service Principal (Graph API Authentication)
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, Client Secret authentication is supported. The Service Principal must have the following API Permission:
-- **_Microsoft Graph Application Permissions_**:
- - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
-
-> For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
-
-Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
-
-#### Client Certificate or Client Secret
-
-Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
-
-* **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-* **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
+## Enterprise Application (Service Principal)
- The certificate can be in either of the following formats:
- * Base64-encoded PKCS#12 (PFX) with a matching private key.
- * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
-
- If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-> **Creating and Formatting a Client Certificate using Keyfactor Command**
->
-> To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
->
-> 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
-> 2. Select a **Template** that supports Client Authentication as an extended key usage.
-> 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
-> 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
-> 5. Make a note of the password on the next page - it won't be shown again.
-> 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
-> * If you downloaded the certificate in PEM format, use the commands below:
->
-> ```shell
-> # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
-> cat
->
-> # Separate the certificate from the private key
-> openssl x509 -in -out pubkeycert.pem
->
-> # Base64 encode the certificate and private key
-> cat | base64 > clientcertkeypair.pem.base64
-> ```
->
-> * If you downloaded the certificate in PFX format, use the commands below:
->
-> ```shell
-> # Export the certificate from the PFX file
-> openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
->
-> # Base64 encode the PFX file
-> cat | base64 > clientcert.pfx.base64
-> ```
-> 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
->
-> You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
-
-### Enterprise Application (Service Principal)
-
-#### Service Principal Certificates
+### Service Principal Certificates
Service Principal certificates are typically used for SAML Token signing. Service Principals are created from Enterprise Applications, and will mostly be configured with a variation of Microsoft's [SAML-based single sign-on](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal) documentation. For more information on the mechanics of the Service Principal certificate management capabilities of this extension, please see the [mechanics](#extension-mechanics) section.
-# Extension Mechanics
-
-The Azure App Registration and Enterprise Application Orchestrator extension uses the [Microsoft Dotnet Graph SDK](https://learn.microsoft.com/en-us/graph/sdks/sdks-overview) to interact with the Microsoft Graph API. The extension uses the following Graph API endpoints to manage Service Principal certificates:
-
-* [Get Service Principal](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0&tabs=http) - Used to obtain the Object ID of the Enterprise Application, and to download the certificates owned by the Service Principal.
-* [Update Service Principal](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-update?view=graph-rest-1.0&tabs=http) - Used to modify the Enterprise Application to add or remove certificates.
- * Specifically, the extension manipulates the [`keyCredentials` resource](https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) of the Service Principal object.
-
-### Discovery Job
+# Discovery Job Configuration
The Discovery operation discovers all Azure Enterprise Applications that the Service Principal has access to. The discovered Enterprise Applications (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
diff --git a/docsource/azuresp2.md b/docsource/azuresp2.md
new file mode 100644
index 0000000..0e36d49
--- /dev/null
+++ b/docsource/azuresp2.md
@@ -0,0 +1,21 @@
+# Overview
+
+The Azure Enterprise Application/Service Principal certificate operations are implemented by the `AzureSP` store type, and supports the management of a single certificate for use in SSO/SAML assertion signing. The Management Add operation is only supported with the certificate replacement option, since adding a new certificate will replace the existing certificate. The Add operation will also set newly added certificates as the active certificate for SSO/SAML usage. The Management Remove operation removes the certificate from the Enterprise Application/Service Principal, which is the same as removing the SSO/SAML signing certificate. The Discovery operation discovers all Enterprise Applications/Service Principals in the tenant.
+
+# Requirements
+
+## Enterprise Application (Service Principal)
+
+### Service Principal Certificates
+
+Service Principal certificates are typically used for SAML Token signing. Service Principals are created from Enterprise Applications, and will mostly be configured with a variation of Microsoft's [SAML-based single sign-on](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal) documentation. For more information on the mechanics of the Service Principal certificate management capabilities of this extension, please see the [mechanics](#extension-mechanics) section.
+
+# Discovery Job Configuration
+
+The Discovery operation discovers all Azure Enterprise Applications that the Service Principal has access to. The discovered Enterprise Applications (specifically, their Application IDs) are reported back to Command and can be easily added as certificate stores from the Locations tab.
+
+The Discovery operation uses the "Directories to search" field, and accepts input in one of the following formats:
+- `*` - If the asterisk symbol `*` is used, the extension will search for all Azure Enterprise Applications that the Service Principal has access to, but only in the tenant that the discovery job was configured for as specified by the "Client Machine" field in the certificate store configuration.
+- `,,...` - If a comma-separated list of tenant IDs is used, the extension will search for all Azure Enterprise Applications available in each tenant specified in the list. The tenant IDs should be the GUIDs associated with each tenant, and it's the user's responsibility to ensure that the service principal has access to the specified tenants.
+
+> The Discovery Job only supports Client Secret authentication.
diff --git a/docsource/content.md b/docsource/content.md
new file mode 100644
index 0000000..e5aab13
--- /dev/null
+++ b/docsource/content.md
@@ -0,0 +1,80 @@
+## Overview
+
+The Azure App Registration and Enterprise Application Orchestrator extension remotely manages both Azure [App Registration/Application](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) certificates and [Enterprise Application/Service Principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/enterprise-apps-certificate-credentials) certificates. Application certificates are typically public key only and used for client certificate authentication, while Service Principal certificates are commonly used for [SAML Assertion signing](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/tutorial-manage-certificates-for-federated-single-sign-on). The extension implements the Inventory, Management Add, Management Remove, and Discovery job types.
+
+Certificates used for client authentication by Applications (configured in App Registrations) are represented by the [`AzureApp` store type](docs/azureapp.md), and certificates used for SSO/SAML assertion signing are represented by the [`AzureSP` store type](docs/azuresp.md). Both store types are managed by the same extension. The extension is configured with a single Azure Service Principal that is used to authenticate to the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). The Azure App Registration and Enterprise Application Orchestrator extension manages certificates for Azure App Registrations (Applications) and Enterprise Applications (Service Principals) differently.
+
+## Requirements
+
+### Azure Service Principal (Graph API Authentication)
+
+The Azure App Registration and Enterprise Application Orchestrator extension uses an [Azure Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) for authentication. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) to create a service principal. Currently, both Client Secret authentication and Client Certificate authentication (mTLS) are supported.
+
+The Service Principal must have the following API Permission:
+- **_Microsoft Graph Application Permissions_**:
+ - `Application.ReadWrite.All` (_not_ Delegated; Admin Consent) - Allows the app to create, read, update and delete applications and service principals without a signed-in user.
+
+> For more information on Admin Consent for App-only access (also called "Application Permissions"), see the [primer on application-only access](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-only-access-primer).
+
+Alternatively, the Service Principal can be granted the `Application.ReadWrite.OwnedBy` permission if the Service Principal is only intended to manage its own App Registration/Application.
+
+#### Client Certificate or Client Secret
+
+Beginning in version 3.0.0, the Azure App Registration and Enterprise Application Orchestrator extension supports both [client certificate authentication](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) and [client secret](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) authentication.
+
+* **Client Secret** - Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-2-add-a-client-secret) to create a Client Secret. This secret will be used as the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+* **Client Certificate** - Create a client certificate key pair with the Client Authentication extended key usage. The client certificate will be used in the ClientCertificate field in the [Certificate Store Configuration](#certificate-store-configuration) section. If you have access to Keyfactor Command, the instructions in this section walk you through enrolling a certificate and ensuring that it's in the correct format. Once enrolled, follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the _public key_ certificate (no private key) to the service principal used for authentication.
+
+ The certificate can be in either of the following formats:
+ * Base64-encoded PKCS#12 (PFX) with a matching private key.
+ * Base64-encoded PEM-encoded certificate _and_ PEM-encoded PKCS8 private key. Make sure that the certificate and private key are separated with a newline. The order doesn't matter - the extension will determine which is which.
+
+ If the private key is encrypted, the encryption password will replace the **Server Password** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+
+> **Creating and Formatting a Client Certificate using Keyfactor Command**
+>
+> To get started quickly, you can follow the instructions below to create and properly format a client certificate to authenticate to the Microsoft Graph API.
+>
+> 1. In Keyfactor Command, hover over **Enrollment** and select **PFX Enrollment**.
+> 2. Select a **Template** that supports Client Authentication as an extended key usage.
+> 3. Populate the certificate subject as appropriate for the Template. It may be sufficient to only populate the Common Name, but consult your IT policy to ensure that this certificate is compliant.
+> 4. At the bottom of the page, uncheck the box for **Include Chain**, and select either **PFX** or **PEM** as the certificate Format.
+> 5. Make a note of the password on the next page - it won't be shown again.
+> 6. Prepare the certificate and private key for Azure and the Orchestrator extension:
+> * If you downloaded the certificate in PEM format, use the commands below:
+>
+> ```shell
+> # Verify that the certificate downloaded from Command contains the certificate and private key. They should be in the same file
+> cat
+>
+> # Separate the certificate from the private key
+> openssl x509 -in -out pubkeycert.pem
+>
+> # Base64 encode the certificate and private key
+> cat | base64 > clientcertkeypair.pem.base64
+> ```
+>
+> * If you downloaded the certificate in PFX format, use the commands below:
+>
+> ```shell
+> # Export the certificate from the PFX file
+> openssl pkcs12 -in -clcerts -nokeys -out pubkeycert.pem
+>
+> # Base64 encode the PFX file
+> cat | base64 > clientcert.pfx.base64
+> ```
+> 7. Follow [Microsoft's documentation](https://learn.microsoft.com/en-us/graph/auth-register-app-v2#option-1-add-a-certificate) to add the public key certificate to the service principal used for authentication.
+>
+> You will use `clientcert.[pem|pfx].base64` as the **ClientCertificate** field in the [Certificate Store Configuration](#certificate-store-configuration) section.
+
+## Discovery
+
+> The Discovery Job for all four Certificate Store Types implemented by the Azure App Registration and Enterprise Application Orchestrator extension returns Store Paths in the format ` ()`. When defining Certificate Stores manually, you may elect to follow this format, or use the standard `` for the Store Path.
+
+## Extension Mechanics
+
+The Azure App Registration and Enterprise Application Orchestrator extension uses the [Microsoft Dotnet Graph SDK](https://learn.microsoft.com/en-us/graph/sdks/sdks-overview) to interact with the Microsoft Graph API. The extension uses the following Graph API endpoints to manage Application certificates:
+
+* [Get Application](https://learn.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http) - Used to obtain the Object ID of the App Registration, and to download the certificates owned by the App Registration.
+* [Update Application](https://learn.microsoft.com/en-us/graph/api/application-update?view=graph-rest-1.0&tabs=http) - Used to modify the App Registration to add or remove certificates.
+ * Specifically, the extension manipulates the [`keyCredentials` resource](https://learn.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0) of the Application object.
diff --git a/docsource/images/AzureApp-advanced-store-type-dialog.png b/docsource/images/AzureApp-advanced-store-type-dialog.png
index 534ecb221e9b4c7efda1bab9b8be958425776ed1..ac0509854a616af229474b084d602789394d0c94 100644
GIT binary patch
literal 41694
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