Skip to content

Commit ffedea0

Browse files
committed
Add RBAC role assignment support for resource group security
• Add RoleAssignmentTask to assign RBAC roles to Azure resources in a resource group • Extract ServicePrincipalResolver to resolve object ID from client credentials, shared by RoleAssignmentTask and KeyVaultTasks • Add ResourceSecurityInstallJob to assign Reader role to the runtime account • Integrate ResourceSecurityInstallJob into SolutionInstaller, continuing on failure • Add Azure.ResourceManager.Authorization package dependency
1 parent 0d50d99 commit ffedea0

File tree

10 files changed

+198
-21
lines changed

10 files changed

+198
-21
lines changed

src/AnalyticsEngine/App.ControlPanel.Engine/App.ControlPanel.Engine.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
<Compile Include="InstallerTasks\JobTasks\AutomationAccountTask.cs" />
7979
<Compile Include="ConfigureAzureComponentsTasks.cs" />
8080
<Compile Include="InstallerTasks\JobTasks\RunbookCreateOrUpdateTasks.cs" />
81+
<Compile Include="InstallerTasks\ResourceSecurityInstallJob.cs" />
8182
<Compile Include="InstallerTasks\RunbooksInstallJob.cs" />
8283
<Compile Include="Models\AzStorageConnectionInfo.cs" />
8384
<Compile Include="SolutionUninstaller.cs" />

src/AnalyticsEngine/App.ControlPanel.Engine/InstallerTasks/AzurePaaSInstallJob.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace App.ControlPanel.Engine.InstallerTasks
2121
/// </summary>
2222
public class AzurePaaSInstallJob : BaseAnalyticsSolutionInstallJob
2323
{
24+
private readonly GetOrCreateResourceGroupTask _rgCreateTask;
2425
private readonly AutomationAccountTask _automationAccountTask;
2526

2627
private readonly SqlServerTask _sqlServerTask;
@@ -44,6 +45,12 @@ public class AzurePaaSInstallJob : BaseAnalyticsSolutionInstallJob
4445
/// </summary>
4546
public AzurePaaSInstallJob(ILogger logger, SolutionInstallConfig config, SubscriptionResource subscription) : base(logger, config, subscription)
4647
{
48+
49+
var tagDic = config.Tags.ToDictionary();
50+
51+
_rgCreateTask = new GetOrCreateResourceGroupTask(TaskConfig.GetConfigForName(config.ResourceGroupName), logger, Location, tagDic, subscription);
52+
this.AddTask(_rgCreateTask);
53+
4754
// Performance levels
4855
var appPerfTier = AppServicePlanTask.PERF_TIER_BASIC1;
4956
var sqlPerfTier = SqlDatabaseTask.PERF_TIER_BASIC;
@@ -54,8 +61,6 @@ public AzurePaaSInstallJob(ILogger logger, SolutionInstallConfig config, Subscri
5461
appPerfTier = AppServicePlanTask.PERF_TIER_BASIC2;
5562
}
5663

57-
var tagDic = config.Tags.ToDictionary();
58-
5964
// Web
6065
var appServicePlanConfig = TaskConfig.GetConfigForName(config.AppServiceWebAppName).AddSetting(AppServicePlanTask.CONFIG_KEY_PERF_TIER, appPerfTier);
6166
_appServicePlanTask = new AppServicePlanTask(appServicePlanConfig, logger, Location, tagDic);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Azure.ResourceManager.Authorization;
2+
using Azure.ResourceManager.Resources;
3+
using CloudInstallEngine;
4+
using CloudInstallEngine.Azure.InstallTasks;
5+
using Common.Entities.Installer;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace App.ControlPanel.Engine.InstallerTasks
9+
{
10+
/// <summary>
11+
/// Secures resources in the resource group by assigning RBAC roles.
12+
/// </summary>
13+
public class ResourceSecurityInstallJob : BaseAnalyticsSolutionInstallJob
14+
{
15+
private readonly RoleAssignmentTask _appInsightsReaderRoleTask;
16+
17+
public ResourceSecurityInstallJob(ILogger logger, SolutionInstallConfig config, SubscriptionResource subscription) : base(logger, config, subscription)
18+
{
19+
var tagDic = config.Tags.ToDictionary();
20+
21+
// Assign Reader role to the runtime account on the resource group (covers App Insights and all resources)
22+
var readerRoleConfig = TaskConfig.GetConfigForPropAndVal(RoleAssignmentTask.CONFIG_KEY_ROLE_NAME, "Reader")
23+
.AddSetting(RoleAssignmentTask.CONFIG_KEY_CLIENT_ID, config.RuntimeAccountOffice365.ClientId)
24+
.AddSetting(RoleAssignmentTask.CONFIG_KEY_CLIENT_SECRET, config.RuntimeAccountOffice365.Secret)
25+
.AddSetting(RoleAssignmentTask.CONFIG_KEY_TENANT_ID, config.RuntimeAccountOffice365.DirectoryId)
26+
.AddSetting(RoleAssignmentTask.CONFIG_KEY_PRINCIPAL_TYPE, "ServicePrincipal");
27+
28+
_appInsightsReaderRoleTask = new RoleAssignmentTask(readerRoleConfig, logger, Location, tagDic);
29+
this.AddTask(_appInsightsReaderRoleTask);
30+
}
31+
32+
public RoleAssignmentResource AppInsightsReaderRole => GetTaskResult<RoleAssignmentResource>(_appInsightsReaderRoleTask);
33+
}
34+
}

src/AnalyticsEngine/App.ControlPanel.Engine/SolutionInstaller.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ public async Task InstallOrUpdate()
4848
var azureBackeEndCreationJob = new AzurePaaSInstallJob(_logger, Config, azureSub);
4949
await azureBackeEndCreationJob.Install();
5050

51+
// Secure resources with RBAC roles
52+
try
53+
{
54+
var resourceSecurityJob = new ResourceSecurityInstallJob(_logger, Config, azureSub);
55+
await resourceSecurityJob.Install();
56+
}
57+
catch (Exception ex)
58+
{
59+
_logger.LogError($"Failed to assign RBAC roles: {ex.Message}. Continuing installation...");
60+
}
61+
5162
// Run stuff now everything in Azure is created
5263
var tasks = new ConfigureAzureComponentsTasks(Config, _logger, _ftpConfig, InstalledByUsername, _softwareConfig, _configPassword);
5364
await tasks.RunPostCreatePaaSTasks(

src/AnalyticsEngine/Common/CloudInstallEngine/Azure/InstallTasks/KeyVaultTasks.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Microsoft.Extensions.Logging;
1010
using System;
1111
using System.Collections.Generic;
12-
using System.IdentityModel.Tokens.Jwt;
1312
using System.Linq;
1413
using System.Threading.Tasks;
1514

@@ -109,18 +108,10 @@ protected async Task AddPolicyForConfiguredRuntimeAccount(KeyVaultResource vault
109108
_logger.LogInformation($"Adding Azure AD application with client ID '{clientId}' to key vault {vaultResource.Data.Name} for secret read & list; certificate read");
110109

111110
// Extract object Id by getting a token from the credentials passed
112-
var creds = new ClientSecretCredential(tenantId.ToString(), clientId, secret);
113-
var credTokenResponse = await creds.GetTokenAsync(new TokenRequestContext(new string[] { "https://management.core.windows.net/.default" }, null));
114-
var handler = new JwtSecurityTokenHandler();
115-
var jwtSecurityToken = handler.ReadJwtToken(credTokenResponse.Token);
116-
var objectId = jwtSecurityToken.Claims.Where(c => c.Type == "oid").FirstOrDefault();
117-
if (objectId == null)
118-
{
119-
throw new InstallException($"No object ID found for client credentials");
120-
}
121-
_logger.LogInformation($"Detected client ID '{clientId}' has object ID '{objectId.Value}'");
111+
var objectIdValue = await ServicePrincipalResolver.GetObjectIdFromClientCredentials(tenantId.ToString(), clientId, secret);
112+
_logger.LogInformation($"Detected client ID '{clientId}' has object ID '{objectIdValue}'");
122113

123-
await AddPolicyForConfiguredAccount(vaultResource, tenantId, objectId.Value, secretPerms, certPerms);
114+
await AddPolicyForConfiguredAccount(vaultResource, tenantId, objectIdValue, secretPerms, certPerms);
124115
}
125116

126117
protected Guid TenantGuidFromConfig()

src/AnalyticsEngine/Common/CloudInstallEngine/Azure/InstallTasks/ResourceGroupTask.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public ResourceGroupContainerLoader(TaskConfig config, ILogger logger, Subscript
2929
public override async Task<ResourceGroupResource> ExecuteTaskReturnResult(object contextArg)
3030
{
3131
var rgTask = new GetOrCreateResourceGroupTask(_config, _logger, _location, _tags, _subscription);
32-
var rg = await rgTask.GetOrCreateResourceGroup(true);
32+
var rg = await rgTask.GetOrCreateResourceGroup(true, false);
3333

3434
return rg;
3535
}
@@ -53,10 +53,10 @@ public GetOrCreateResourceGroupTask(TaskConfig config, ILogger logger, AzureLoca
5353

5454
public override async Task<object> ExecuteTask(object contextArg)
5555
{
56-
return await GetOrCreateResourceGroup(true);
56+
return await GetOrCreateResourceGroup(true, true);
5757
}
5858

59-
public async Task<ResourceGroupResource> GetOrCreateResourceGroup(bool createIfNotExists)
59+
public async Task<ResourceGroupResource> GetOrCreateResourceGroup(bool createIfNotExists, bool verboseLogging)
6060
{
6161
var resourceGroups = _subscription.GetResourceGroups();
6262
ResourceGroupResource resourceGroup = null;
@@ -71,7 +71,10 @@ public async Task<ResourceGroupResource> GetOrCreateResourceGroup(bool createIfN
7171

7272
if (resourceGroup != null)
7373
{
74-
_logger.LogInformation($"Have already resource-group '{_config.ResourceName}'.");
74+
if (verboseLogging)
75+
{
76+
_logger.LogInformation($"Have already resource-group '{_config.ResourceName}'.");
77+
}
7578
if (createIfNotExists)
7679
{
7780
await base.EnsureTagsOnExisting(resourceGroup.Data.Tags, _tags, resourceGroup.GetTagResource());
@@ -82,18 +85,25 @@ public async Task<ResourceGroupResource> GetOrCreateResourceGroup(bool createIfN
8285
// Create
8386
if (createIfNotExists)
8487
{
85-
Console.WriteLine($"Creating resource-group '{_config.ResourceName}'...");
88+
if (verboseLogging)
89+
{
90+
Console.WriteLine($"Creating resource-group '{_config.ResourceName}'...");
91+
}
8692

8793
var resourceGroupData = new ResourceGroupData(_location);
8894
base.EnsureTagsOnNew(resourceGroupData.Tags, _tags);
8995
var operation = await resourceGroups.CreateOrUpdateAsync(WaitUntil.Completed, _config.ResourceName, resourceGroupData);
9096

9197
_logger.LogInformation($"Created resource-group '{_config.ResourceName}'.");
98+
9299
return operation.Value;
93100
}
94101
else
95102
{
96-
_logger.LogInformation($"Can't find resource-group '{_config.ResourceName}', but no errors in calling Azure APIs.");
103+
if (verboseLogging)
104+
{
105+
_logger.LogInformation($"Can't find resource-group '{_config.ResourceName}', but no errors in calling Azure APIs.");
106+
}
97107
return null;
98108
}
99109

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using Azure;
2+
using Azure.Core;
3+
using Azure.ResourceManager.Authorization;
4+
using Azure.ResourceManager.Authorization.Models;
5+
using CloudInstallEngine.Models;
6+
using Microsoft.Extensions.Logging;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using System.Threading.Tasks;
11+
12+
namespace CloudInstallEngine.Azure.InstallTasks
13+
{
14+
/// <summary>
15+
/// Assigns an RBAC role to an Azure resource within a resource group.
16+
/// </summary>
17+
public class RoleAssignmentTask : InstallTaskInAzResourceGroup<RoleAssignmentResource>
18+
{
19+
public const string CONFIG_KEY_ROLE_NAME = "roleName";
20+
public const string CONFIG_KEY_PRINCIPAL_TYPE = "principalType";
21+
public const string CONFIG_KEY_CLIENT_ID = "clientId";
22+
public const string CONFIG_KEY_CLIENT_SECRET = "clientSecret";
23+
public const string CONFIG_KEY_TENANT_ID = "tenantId";
24+
25+
public RoleAssignmentTask(TaskConfig config, ILogger logger, AzureLocation azureLocation, Dictionary<string, string> tags)
26+
: base(config, logger, azureLocation, tags)
27+
{
28+
}
29+
30+
public override string TaskName => "assign RBAC role";
31+
32+
public override async Task<RoleAssignmentResource> ExecuteTaskReturnResult(object contextArg)
33+
{
34+
var roleName = _config.GetConfigValue(CONFIG_KEY_ROLE_NAME);
35+
var clientId = _config.GetConfigValue(CONFIG_KEY_CLIENT_ID);
36+
var clientSecret = _config.GetConfigValue(CONFIG_KEY_CLIENT_SECRET);
37+
var tenantId = _config.GetConfigValue(CONFIG_KEY_TENANT_ID);
38+
39+
// Resolve the service principal object ID from client credentials
40+
var objectIdStr = await ServicePrincipalResolver.GetObjectIdFromClientCredentials(tenantId, clientId, clientSecret);
41+
_logger.LogInformation($"Resolved client ID '{clientId}' to object ID '{objectIdStr}'");
42+
43+
if (!Guid.TryParse(objectIdStr, out var principalId))
44+
{
45+
throw new InstallException($"Invalid object ID '{objectIdStr}' resolved for client ID '{clientId}'");
46+
}
47+
48+
// Find role definition by name on the resource group scope
49+
var scope = Container.Id;
50+
AuthorizationRoleDefinitionResource roleDefinition = null;
51+
foreach (var rd in Container.GetAuthorizationRoleDefinitions())
52+
{
53+
if (string.Equals(rd.Data.RoleName, roleName, StringComparison.OrdinalIgnoreCase))
54+
{
55+
roleDefinition = rd;
56+
break;
57+
}
58+
}
59+
if (roleDefinition == null)
60+
{
61+
throw new InstallException($"Role definition '{roleName}' not found on scope '{scope}'");
62+
}
63+
64+
// Check for existing assignment
65+
foreach (var existing in Container.GetRoleAssignments())
66+
{
67+
if (existing.Data.PrincipalId == principalId &&
68+
existing.Data.RoleDefinitionId == roleDefinition.Id)
69+
{
70+
_logger.LogInformation($"Role '{roleName}' already assigned to principal '{principalId}'.");
71+
return existing;
72+
}
73+
}
74+
75+
// Create new role assignment
76+
_logger.LogInformation($"Assigning role '{roleName}' to principal '{principalId}'...");
77+
var roleAssignmentId = Guid.NewGuid().ToString();
78+
var content = new RoleAssignmentCreateOrUpdateContent(roleDefinition.Id, principalId);
79+
80+
if (_config.ContainsKey(CONFIG_KEY_PRINCIPAL_TYPE))
81+
{
82+
content.PrincipalType = new RoleManagementPrincipalType(_config.GetConfigValue(CONFIG_KEY_PRINCIPAL_TYPE));
83+
}
84+
85+
var result = await Container.GetRoleAssignments().CreateOrUpdateAsync(
86+
WaitUntil.Completed, roleAssignmentId, content);
87+
88+
_logger.LogInformation($"Role '{roleName}' assigned successfully.");
89+
return result.Value;
90+
}
91+
}
92+
}

src/AnalyticsEngine/Common/CloudInstallEngine/Azure/ResourceGroupTestOnlyInstallJob.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public override async Task Install()
2424
var t = new GetOrCreateResourceGroupTask(TaskConfig.GetConfigForName(ResourceGroupName), Logger, Location, new Dictionary<string, string>(), _subscription);
2525

2626
// Remember group found
27-
_resourceGroupFound = await t.GetOrCreateResourceGroup(false);
27+
_resourceGroupFound = await t.GetOrCreateResourceGroup(false, true);
2828
}
2929

3030
public ResourceGroupResource ResourceGroupFound => _resourceGroupFound;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Azure.Core;
2+
using Azure.Identity;
3+
using CloudInstallEngine.Models;
4+
using System.IdentityModel.Tokens.Jwt;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
8+
namespace CloudInstallEngine.Azure
9+
{
10+
/// <summary>
11+
/// Resolves the Entra ID object ID (service principal) from app registration client credentials.
12+
/// </summary>
13+
public static class ServicePrincipalResolver
14+
{
15+
/// <summary>
16+
/// Gets the object ID of a service principal by authenticating with client credentials and reading the "oid" claim from the resulting JWT.
17+
/// </summary>
18+
public static async Task<string> GetObjectIdFromClientCredentials(string tenantId, string clientId, string clientSecret)
19+
{
20+
var creds = new ClientSecretCredential(tenantId, clientId, clientSecret);
21+
var credTokenResponse = await creds.GetTokenAsync(new TokenRequestContext(new string[] { "https://management.core.windows.net/.default" }, null));
22+
var handler = new JwtSecurityTokenHandler();
23+
var jwtSecurityToken = handler.ReadJwtToken(credTokenResponse.Token);
24+
var objectIdClaim = jwtSecurityToken.Claims.Where(c => c.Type == "oid").FirstOrDefault();
25+
if (objectIdClaim == null)
26+
{
27+
throw new InstallException("No object ID found in token for the given client credentials");
28+
}
29+
return objectIdClaim.Value;
30+
}
31+
}
32+
}

src/AnalyticsEngine/Common/CloudInstallEngine/CloudInstallEngine.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ItemGroup>
88
<PackageReference Include="Azure.Identity" Version="1.11.4" />
99
<PackageReference Include="Azure.ResourceManager.AppService" Version="1.0.0" />
10+
<PackageReference Include="Azure.ResourceManager.Authorization" Version="1.1.0" />
1011
<PackageReference Include="Azure.ResourceManager.CognitiveServices" Version="1.2.0" />
1112
<PackageReference Include="Azure.ResourceManager.KeyVault" Version="1.1.0" />
1213
<PackageReference Include="Azure.ResourceManager.OperationalInsights" Version="1.1.0" />

0 commit comments

Comments
 (0)