Skip to content

Commit af1ede2

Browse files
ferantiverockittel
andauthored
feat (infra): [ai-foundry-sec] bcdr replace system by user assigned identity (#75)
* bcdr mitigation: switch from system to user assigned for re-creation scenarios * add cors configuration for the sto account * Address PR Feedback: no longer rquired (old AI Foundry portal model) Revert "add cors configuration for the sto account" This reverts commit cd19d08. * Address PR Feedback: KISS for UMI - no dep desired * Address PR Feedback: projectId not desired with UMI * Address PR Feedback: rbac within each resouce module when possible like debug Revert "Address PR Feedback: KISS for UMI - no dep desired" This reverts commit dfcfa05. * Address PR Feedback: account must no be UMI This reverts commit d4c2f84. * Address PR Feedback: remove redundant depedency on rbac * Address PR Feedback: KISS take#2 for UMI module - no dep desired * Address PR Feedback: rbac naming simplied further Co-authored-by: Chad Kittel <chad.kittel@gmail.com> --------- Co-authored-by: Chad Kittel <chad.kittel@gmail.com>
1 parent 6dbc507 commit af1ede2

10 files changed

+116
-101
lines changed

infra-as-code/bicep/ai-agent-blob-storage.bicep

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,28 @@ param logAnalyticsWorkspaceName string
2222
@minLength(1)
2323
param privateEndpointSubnetResourceId string
2424

25+
@description('The existing User Managed Identity for the AI Foundry project.')
26+
@minLength(1)
27+
param existingAgentUserManagedIdentityName string
28+
2529
// ---- Existing resources ----
2630

31+
@description('Existing Agent User Managed Identity for the AI Foundry Project.')
32+
resource agentUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = {
33+
name: existingAgentUserManagedIdentityName
34+
}
35+
2736
resource storageBlobDataOwnerRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
2837
name: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
2938
scope: subscription()
3039
}
3140

41+
// Storage Blob Data Contributor
42+
resource storageBlobDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
43+
name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
44+
scope: subscription()
45+
}
46+
3247
resource blobStorageLinkedPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = {
3348
name: 'privatelink.blob.${environment().suffixes.storage}'
3449
}
@@ -96,6 +111,15 @@ resource debugUserBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignmen
96111
}
97112
}
98113

114+
@description('Grant the AI Foundry Project managed identity Storage Account Blob Data Contributor user role permissions.')
115+
module projectBlobDataContributorAssignment './modules/storageAccountRoleAssignment.bicep' = {
116+
name: 'projectBlobDataContributorAssignmentDeploy'
117+
params: {
118+
roleDefinitionId: storageBlobDataContributorRole.id
119+
principalId: agentUserManagedIdentity.properties.principalId
120+
existingStorageAccountName: agentStorageAccount.name
121+
}
122+
}
99123

100124
// Private endpoints
101125

infra-as-code/bicep/ai-agent-service-dependencies.bicep

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ param privateEndpointSubnetResourceId string
2424

2525
// ---- New resources ----
2626

27+
@description('The agent User Managed Identity for the AI Foundry Project. This is used when a user uploads a file to the agent, and the agent needs to search for information in that file.')
28+
resource agentUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' = {
29+
name: 'mi-agent-${baseName}'
30+
location: location
31+
}
32+
2733
@description('Deploy Azure Storage account for the Azure AI Foundry Agent Service (dependency). This is used for binaries uploaded within threads or as "knowledge" uploaded as part of an agent.')
2834
module deployAgentStorageAccount 'ai-agent-blob-storage.bicep' = {
2935
name: 'agentStorageAccountDeploy'
@@ -34,6 +40,7 @@ module deployAgentStorageAccount 'ai-agent-blob-storage.bicep' = {
3440
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
3541
debugUserPrincipalId: debugUserPrincipalId
3642
privateEndpointSubnetResourceId: privateEndpointSubnetResourceId
43+
existingAgentUserManagedIdentityName: agentUserManagedIdentity.name
3744
}
3845
}
3946

@@ -47,6 +54,7 @@ module deployCosmosDbThreadStorageAccount 'cosmos-db.bicep' = {
4754
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
4855
debugUserPrincipalId: debugUserPrincipalId
4956
privateEndpointSubnetResourceId: privateEndpointSubnetResourceId
57+
existingAgentUserManagedIdentityName: agentUserManagedIdentity.name
5058
}
5159
}
5260

@@ -60,6 +68,7 @@ module deployAzureAISearchService 'ai-search.bicep' = {
6068
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
6169
debugUserPrincipalId: debugUserPrincipalId
6270
privateEndpointSubnetResourceId: privateEndpointSubnetResourceId
71+
existingAgentUserManagedIdentityName: agentUserManagedIdentity.name
6372
}
6473
}
6574

@@ -68,3 +77,4 @@ module deployAzureAISearchService 'ai-search.bicep' = {
6877
output cosmosDbAccountName string = deployCosmosDbThreadStorageAccount.outputs.cosmosDbAccountName
6978
output storageAccountName string = deployAgentStorageAccount.outputs.storageAccountName
7079
output aiSearchName string = deployAzureAISearchService.outputs.aiSearchName
80+
output agentUserManagedIdentityName string = agentUserManagedIdentity.name

infra-as-code/bicep/ai-foundry-project.bicep

Lines changed: 18 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ param existingBingAccountName string
2828
@minLength(1)
2929
param existingWebApplicationInsightsResourceName string
3030

31+
@description('The existing User Managed Identity for the AI Foundry project.')
32+
@minLength(1)
33+
param existingAgentUserManagedIdentityName string
34+
3135
// ---- Existing resources ----
3236

37+
@description('Existing Agent User Managed Identity for the AI Foundry Project.')
38+
resource agentUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = {
39+
name: existingAgentUserManagedIdentityName
40+
}
41+
3342
@description('The internal ID of the project is used in the Azure Storage blob containers and in the Cosmos DB collections.')
3443
#disable-next-line BCP053
3544
var workspaceId = aiFoundry::project.properties.internalId
@@ -57,33 +66,12 @@ resource azureAISearchService 'Microsoft.Search/searchServices@2025-02-01-previe
5766
name: existingAISearchAccountName
5867
}
5968

60-
resource azureAISearchServiceContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
61-
name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0'
62-
scope: subscription()
63-
}
64-
65-
resource azureAISearchIndexDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
66-
name: '8ebe5a00-799e-43f5-93ac-243d3dce84a7'
67-
scope: subscription()
68-
}
69-
70-
// Storage Blob Data Contributor
71-
resource storageBlobDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
72-
name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
73-
scope: subscription()
74-
}
75-
7669
// Storage Blob Data Owner Role
7770
resource storageBlobDataOwnerRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
7871
name: 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
7972
scope: subscription()
8073
}
8174

82-
resource cosmosDbOperatorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
83-
name: '230815da-be43-4aae-9cb4-875f7bd000aa'
84-
scope: subscription()
85-
}
86-
8775
#disable-next-line BCP081
8876
resource bingAccount 'Microsoft.Bing/accounts@2025-05-01-preview' existing = {
8977
name: existingBingAccountName
@@ -103,7 +91,10 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-06-01' existing =
10391
name: 'projchat'
10492
location: location
10593
identity: {
106-
type: 'SystemAssigned'
94+
type: 'UserAssigned'
95+
userAssignedIdentities: {
96+
'${agentUserManagedIdentity.id}': {}
97+
}
10798
}
10899
properties: {
109100
description: 'Chat using internet data in your Azure AI Foundry Agent.'
@@ -123,9 +114,7 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-06-01' existing =
123114
location: cosmosDbAccount.location
124115
}
125116
}
126-
dependsOn: [
127-
projectDbCosmosDbOperatorAssignment
128-
]
117+
dependsOn: []
129118
}
130119

131120
@description('Create project connection to the Azure Storage account; dependency for Azure AI Foundry Agent Service.')
@@ -142,7 +131,6 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-06-01' existing =
142131
}
143132
}
144133
dependsOn: [
145-
projectBlobDataContributorAssignment
146134
projectBlobDataOwnerConditionalAssignment
147135
threadStorageConnection // Single thread these connections, else conflict errors tend to happen
148136
]
@@ -162,8 +150,6 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-06-01' existing =
162150
}
163151
}
164152
dependsOn: [
165-
projectAISearchIndexDataContributorAssignment
166-
projectAISearchContributorAssignment
167153
storageConnection // Single thread these connections, else conflict errors tend to happen
168154
]
169155
}
@@ -190,7 +176,6 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-06-01' existing =
190176
]
191177
}
192178

193-
194179
@description('Create the Azure AI Foundry Agent Service capability.')
195180
resource aiAgentService 'capabilityHosts' = {
196181
name: 'projectagents'
@@ -232,72 +217,26 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-06-01' existing =
232217

233218
// Role assignments
234219

235-
@description('Grant the AI Foundry Project managed identity Cosmos Db Db Operator user role permissions.')
236-
module projectDbCosmosDbOperatorAssignment './modules/cosmosdbRoleAssignment.bicep' = {
237-
name: 'projectDbCosmosDbOperatorAssignmentDeploy'
238-
params: {
239-
roleDefinitionId: cosmosDbOperatorRole.id
240-
principalId: aiFoundry::project.identity.principalId
241-
existingAiFoundryProjectId: aiFoundry::project.id
242-
existingCosmosDbAccountName: existingCosmosDbAccountName
243-
}
244-
}
245-
246-
@description('Grant the AI Foundry Project managed identity Storage Account Blob Data Contributor user role permissions.')
247-
module projectBlobDataContributorAssignment './modules/storageAccountRoleAssignment.bicep' = {
248-
name: 'projectBlobDataContributorAssignmentDeploy'
249-
params: {
250-
roleDefinitionId: storageBlobDataContributorRole.id
251-
principalId: aiFoundry::project.identity.principalId
252-
existingAiFoundryProjectId: aiFoundry::project.id
253-
existingStorageAccountName: existingStorageAccountName
254-
}
255-
}
256-
257220
@description('Grant the AI Foundry Project managed identity Storage Account Blob Data Owner user role permissions.')
258221
module projectBlobDataOwnerConditionalAssignment './modules/storageAccountRoleAssignment.bicep' = {
259222
name: 'projectBlobDataOwnerConditionalAssignmentDeploy'
260223
params: {
261224
roleDefinitionId: storageBlobDataOwnerRole.id
262-
principalId: aiFoundry::project.identity.principalId
263-
existingAiFoundryProjectId: aiFoundry::project.id
225+
principalId: agentUserManagedIdentity.properties.principalId
264226
existingStorageAccountName: existingStorageAccountName
265227
conditionVersion: '2.0'
266228
condition: '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read\'}) AND !(ActionMatches{\'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action\'}) AND !(ActionMatches{\'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write\'}) ) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase \'${workspaceIdAsGuid}\'))'
267229
}
268230
}
269231

270-
@description('Grant the AI Foundry Project managed identity AI Search Contributor user role permissions.')
271-
module projectAISearchContributorAssignment './modules/aiSearchRoleAssignment.bicep' = {
272-
name: 'projectAISearchContributorAssignmentDeploy'
273-
params: {
274-
roleDefinitionId: azureAISearchServiceContributorRole.id
275-
principalId: aiFoundry::project.identity.principalId
276-
existingAiFoundryProjectId: aiFoundry::project.id
277-
existingAISearchAccountName: existingAISearchAccountName
278-
}
279-
}
280-
281-
@description('Grant the AI Foundry Project managed identity AI Search Data Contributor user role permissions.')
282-
module projectAISearchIndexDataContributorAssignment './modules/aiSearchRoleAssignment.bicep' = {
283-
name: 'projectAISearchIndexDataContributorAssignmentDeploy'
284-
params: {
285-
roleDefinitionId: azureAISearchIndexDataContributorRole.id
286-
principalId: aiFoundry::project.identity.principalId
287-
existingAiFoundryProjectId: aiFoundry::project.id
288-
existingAISearchAccountName: existingAISearchAccountName
289-
}
290-
}
291-
292232
// Sql Role Assignments
293233

294234
@description('Assign the project\'s managed identity the ability to read and write data in this collection within enterprise_memory database.')
295235
module projectUserThreadContainerWriterSqlAssignment './modules/cosmosdbSqlRoleAssignment.bicep' = {
296236
name: 'projectUserThreadContainerWriterSqlAssignmentDeploy'
297237
params: {
298238
roleDefinitionId: cosmosDbAccount::dataContributorRole.id
299-
principalId: aiFoundry::project.identity.principalId
300-
existingAiFoundryProjectId: aiFoundry::project.id
239+
principalId: agentUserManagedIdentity.properties.principalId
301240
existingCosmosDbAccountName: existingCosmosDbAccountName
302241
existingCosmosDbName: 'enterprise_memory'
303242
existingCosmosCollectionTypeName: 'user'
@@ -313,8 +252,7 @@ module projectSystemThreadContainerWriterSqlAssignment './modules/cosmosdbSqlRol
313252
name: 'projectSystemThreadContainerWriterSqlAssignmentDeploy'
314253
params: {
315254
roleDefinitionId: cosmosDbAccount::dataContributorRole.id
316-
principalId: aiFoundry::project.identity.principalId
317-
existingAiFoundryProjectId: aiFoundry::project.id
255+
principalId: agentUserManagedIdentity.properties.principalId
318256
existingCosmosDbAccountName: existingCosmosDbAccountName
319257
existingCosmosDbName: 'enterprise_memory'
320258
existingCosmosCollectionTypeName: 'system'
@@ -331,8 +269,7 @@ module projectEntityContainerWriterSqlAssignment './modules/cosmosdbSqlRoleAssig
331269
name: 'projectEntityContainerWriterSqlAssignmentDeploy'
332270
params: {
333271
roleDefinitionId: cosmosDbAccount::dataContributorRole.id
334-
principalId: aiFoundry::project.identity.principalId
335-
existingAiFoundryProjectId: aiFoundry::project.id
272+
principalId: agentUserManagedIdentity.properties.principalId
336273
existingCosmosDbAccountName: existingCosmosDbAccountName
337274
existingCosmosDbName: 'enterprise_memory'
338275
existingCosmosCollectionTypeName: 'entities'

infra-as-code/bicep/ai-search.bicep

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ param privateEndpointSubnetResourceId string
2222
@minLength(36)
2323
param debugUserPrincipalId string
2424

25+
@description('The existing User Managed Identity for the AI Foundry project.')
26+
@minLength(1)
27+
param existingAgentUserManagedIdentityName string
28+
2529
// ---- Existing resources ----
2630

31+
@description('Existing Agent User Managed Identity for the AI Foundry Project.')
32+
resource agentUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = {
33+
name: existingAgentUserManagedIdentityName
34+
}
35+
2736
resource aiSearchLinkedPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = {
2837
name: 'privatelink.search.windows.net'
2938
}
@@ -32,6 +41,11 @@ resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-02
3241
name: logAnalyticsWorkspaceName
3342
}
3443

44+
resource azureAISearchServiceContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
45+
name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0'
46+
scope: subscription()
47+
}
48+
3549
resource azureAISearchIndexDataContributorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
3650
name: '8ebe5a00-799e-43f5-93ac-243d3dce84a7'
3751
scope: subscription()
@@ -76,6 +90,26 @@ resource debugUserAISearchIndexDataContributorAssignment 'Microsoft.Authorizatio
7690
}
7791
}
7892

93+
@description('Grant the AI Foundry Project managed identity AI Search Contributor user role permissions.')
94+
module projectAISearchContributorAssignment './modules/aiSearchRoleAssignment.bicep' = {
95+
name: 'projectAISearchContributorAssignmentDeploy'
96+
params: {
97+
roleDefinitionId: azureAISearchServiceContributorRole.id
98+
principalId: agentUserManagedIdentity.properties.principalId
99+
existingAISearchAccountName: azureAiSearchService.name
100+
}
101+
}
102+
103+
@description('Grant the AI Foundry Project managed identity AI Search Data Contributor user role permissions.')
104+
module projectAISearchIndexDataContributorAssignment './modules/aiSearchRoleAssignment.bicep' = {
105+
name: 'projectAISearchIndexDataContributorAssignmentDeploy'
106+
params: {
107+
roleDefinitionId: azureAISearchIndexDataContributorRole.id
108+
principalId: agentUserManagedIdentity.properties.principalId
109+
existingAISearchAccountName: azureAiSearchService.name
110+
}
111+
}
112+
79113
// Azure diagnostics
80114

81115
@description('Capture Azure Diagnostics for the Azure AI Search Service.')

infra-as-code/bicep/cosmos-db.bicep

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ param debugUserPrincipalId string
2222
@minLength(1)
2323
param privateEndpointSubnetResourceId string
2424

25+
@description('The existing User Managed Identity for the AI Foundry project.')
26+
@minLength(1)
27+
param existingAgentUserManagedIdentityName string
28+
2529
// ---- Existing resources ----
2630

31+
@description('Existing Agent User Managed Identity for the AI Foundry Project.')
32+
resource agentUserManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = {
33+
name: existingAgentUserManagedIdentityName
34+
}
35+
2736
resource cosmosDbLinkedPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = {
2837
name: 'privatelink.documents.azure.com'
2938
}
@@ -34,6 +43,12 @@ resource cosmosDbAccountReaderRole 'Microsoft.Authorization/roleDefinitions@2022
3443
scope: subscription()
3544
}
3645

46+
// Cosmos DB Account Operator Role
47+
resource cosmosDbOperatorRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
48+
name: '230815da-be43-4aae-9cb4-875f7bd000aa'
49+
scope: subscription()
50+
}
51+
3752
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-02-01' existing = {
3853
name: logAnalyticsWorkspaceName
3954
}
@@ -200,6 +215,16 @@ resource assignDebugUserToCosmosAccountReader 'Microsoft.Authorization/roleAssig
200215
}
201216
}
202217

218+
@description('Grant the AI Foundry Project managed identity Cosmos Db Db Operator user role permissions.')
219+
module projectDbCosmosDbOperatorAssignment './modules/cosmosdbRoleAssignment.bicep' = {
220+
name: 'projectDbCosmosDbOperatorAssignmentDeploy'
221+
params: {
222+
roleDefinitionId: cosmosDbOperatorRole.id
223+
principalId: agentUserManagedIdentity.properties.principalId
224+
existingCosmosDbAccountName: cosmosDbAccount.name
225+
}
226+
}
227+
203228
// Prevent Accidental Changes
204229

205230
resource cosmosDbAccountLocks 'Microsoft.Authorization/locks@2020-05-01' = {

infra-as-code/bicep/main.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ module deployAzureAiFoundryProject 'ai-foundry-project.bicep' = {
161161
existingStorageAccountName: deployAIAgentServiceDependencies.outputs.storageAccountName
162162
existingBingAccountName: deployBingAccount.outputs.bingAccountName
163163
existingWebApplicationInsightsResourceName: deployApplicationInsights.outputs.applicationInsightsName
164+
existingAgentUserManagedIdentityName: deployAIAgentServiceDependencies.outputs.agentUserManagedIdentityName
164165
}
165166
dependsOn: [
166167
deployJumpBox

0 commit comments

Comments
 (0)