diff --git a/.env.example b/.env.example index fdc867d..10bfca2 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ # All Values Required AZURE_ENV= ENGINE_APP_ID= -ENGINE_APP_SECRET= UI_APP_ID= TENANT_ID= COSMOS_URL=https://.documents.azure.com @@ -9,3 +8,4 @@ KEYVAULT_URL=https://.vault.azure.net # Legacy Values # COSMOS_KEY= +# ENGINE_APP_SECRET= \ No newline at end of file diff --git a/deploy/deploy.ps1 b/deploy/deploy.ps1 index 9fc0c7b..4382ddf 100644 --- a/deploy/deploy.ps1 +++ b/deploy/deploy.ps1 @@ -721,13 +721,6 @@ process { } } - Write-Host "INFO: Creating Azure IPAM Engine Secret" -ForegroundColor Green - - # Create IPAM Engine Secret - $engineSecret = Invoke-WithGraphRetry -ScriptBlock { - New-AzADAppCredential -ApplicationObject $engineObject -StartDate (Get-Date) -EndDate (Get-Date).AddYears(2) - } - if (-not $DisableUI) { Write-Host "INFO: Azure IPAM Engine & UI Applications/Service Principals created successfully" -ForegroundColor Green } @@ -737,7 +730,6 @@ process { $appDetails = @{ EngineAppId = $engineApp.AppId - EngineSecret = $engineSecret.SecretText } # Add UI AppID to AppDetails (If DisableUI not specified) @@ -894,8 +886,6 @@ process { [string]$UIAppId = [GUID]::Empty, [Parameter(Mandatory = $true)] [string]$EngineAppId, - [Parameter(Mandatory = $true)] - [string]$EngineSecret, [Parameter(Mandatory = $false)] [bool]$DisableUI = $false ) @@ -907,14 +897,13 @@ process { # Update Parameter Values $parametersObject.parameters.engineAppId.value = $EngineAppId - $parametersObject.parameters.engineAppSecret.value = $EngineSecret if (-not $DisableUI) { $parametersObject.parameters.uiAppId.value = $UIAppId - $parametersObject.parameters = $parametersObject.parameters | Select-Object -Property uiAppId, engineAppId, engineAppSecret + $parametersObject.parameters = $parametersObject.parameters | Select-Object -Property uiAppId, engineAppId } else { - $parametersObject.parameters = $parametersObject.parameters | Select-Object -Property engineAppId, engineAppSecret + $parametersObject.parameters = $parametersObject.parameters | Select-Object -Property engineAppId } # Output updated parameter file for Bicep deployment @@ -937,13 +926,12 @@ process { # Read Values from Parameters $UIAppId = $parametersObject.parameters.uiAppId.value ?? [GUID]::Empty $EngineAppId = $parametersObject.parameters.engineAppId.value - $EngineSecret = $parametersObject.parameters.engineAppSecret.value $script:DisableUI = ($UIAppId -eq [GUID]::Empty) ? $true : $false - if ((-not $EngineAppId) -or (-not $EngineSecret)) { + if ((-not $EngineAppId)) { Write-Host "ERROR: Missing required parameters from Bicep parameter file" -ForegroundColor Red Write-Host "ERROR: Please ensure the following parameters are present in the Bicep parameter file" -ForegroundColor Red - Write-Host "ERROR: Required: [engineAppId, engineAppSecret]" -ForegroundColor Red + Write-Host "ERROR: Required: [engineAppId]" -ForegroundColor Red Write-Host "" Write-Host "ERROR: Please refer to the deployment documentation for more information" -ForegroundColor Red Write-Host "ERROR: " -ForegroundColor Red -NoNewline @@ -960,7 +948,6 @@ process { $appDetails = @{ UIAppId = $UIAppId EngineAppId = $EngineAppId - EngineSecret = $EngineSecret } return $appDetails @@ -973,8 +960,6 @@ process { [Parameter(Mandatory = $true)] [string]$EngineAppId, [Parameter(Mandatory = $true)] - [string]$EngineSecret, - [Parameter(Mandatory = $false)] [string]$NamePrefix, [Parameter(Mandatory = $false)] [string]$AzureCloud, @@ -997,7 +982,6 @@ process { # Instantiate deployment parameter object $deploymentParameters = @{ engineAppId = $EngineAppId - engineAppSecret = $EngineSecret uiAppId = $UiAppId } @@ -1160,8 +1144,28 @@ process { } while ($publishSuccess -eq $False -and $publishRetries -ge 0) } - function Update-UIApplication { - param( + Function New-FederatedCredential { + Param( + [Parameter(Mandatory = $true)] + [string]$EngineAppId, + [Parameter(Mandatory = $true)] + [string]$ManagedIdentityPrincipalId + ) + + Write-Host "INFO: Creating Federated Credential" -ForegroundColor Green + + New-AzADAppFederatedCredential ` + -ApplicationObjectId (Get-AzADApplication -ApplicationId $EngineAppId).Id ` + -Audience "api://AzureADTokenExchange" ` + -Issuer "https://login.microsoftonline.com/$($(Get-AzContext).Tenant.Id)/v2.0" ` + -Subject $ManagedIdentityPrincipalId ` + -Name "ipam-engine-mi-credential" | Out-Null + + Write-Host "INFO: Federated Credential Created" -ForegroundColor Green + } + + Function Update-UIApplication { + Param( [Parameter(Mandatory = $true)] [string]$UIAppId, [Parameter(Mandatory = $true)] @@ -1305,6 +1309,12 @@ process { -Endpoint $deployment.Outputs["appServiceHostName"].Value } + if ($PSCmdlet.ParameterSetName -notin 'AppsOnly') { + New-FederatedCredential ` + -EngineAppId $appDetails.EngineAppId ` + -ManagedIdentityPrincipalId $deployment.Outputs["managedIdentityPrincipalId"].Value + } + if ($PSCmdlet.ParameterSetName -in ('App', 'Function')) { if (-not $ZipFilePath) { try { diff --git a/deploy/main.bicep b/deploy/main.bicep index 9f27cee..933544b 100644 --- a/deploy/main.bicep +++ b/deploy/main.bicep @@ -32,10 +32,6 @@ param uiAppId string = '00000000-0000-0000-0000-000000000000' @description('IPAM-Engine App Registration Client/App ID') param engineAppId string -@secure() -@description('IPAM-Engine App Registration Client Secret') -param engineAppSecret string - @description('Tags') param tags object = {} @@ -104,7 +100,6 @@ module keyVault './modules/keyVault.bicep' = { identityClientId: managedIdentity.outputs.clientId uiAppId: uiAppId engineAppId: engineAppId - engineAppSecret: engineAppSecret workspaceId: logAnalyticsWorkspace.outputs.workspaceId } } @@ -200,3 +195,4 @@ output appServiceName string = deployAsFunc ? resourceNames.functionName : resou output appServiceHostName string = deployAsFunc ? functionApp!.outputs.functionAppHostName : appService!.outputs.appServiceHostName output acrName string = privateAcr ? containerRegistry!.outputs.acrName : '' output acrUri string = privateAcr ? containerRegistry!.outputs.acrUri : '' +output managedIdentityPrincipalId string = managedIdentity.outputs.principalId diff --git a/deploy/main.parameters.example.json b/deploy/main.parameters.example.json index e9c5632..af2b037 100644 --- a/deploy/main.parameters.example.json +++ b/deploy/main.parameters.example.json @@ -17,9 +17,6 @@ "engineAppId": { "value": "" }, - "engineAppSecret": { - "value": "" - }, "deployAsFunc": { "value": false }, diff --git a/deploy/modules/appService.bicep b/deploy/modules/appService.bicep index 2aa0b4f..7f22e82 100644 --- a/deploy/modules/appService.bicep +++ b/deploy/modules/appService.bicep @@ -94,10 +94,6 @@ var appServiceSiteConfig = { name: 'ENGINE_APP_ID' value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-ID/)' } - { - name: 'ENGINE_APP_SECRET' - value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-SECRET/)' - } { name: 'TENANT_ID' value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/TENANT-ID/)' diff --git a/deploy/modules/functionApp.bicep b/deploy/modules/functionApp.bicep index 24add10..8ab03fa 100644 --- a/deploy/modules/functionApp.bicep +++ b/deploy/modules/functionApp.bicep @@ -95,10 +95,6 @@ var functionAppSiteConfig = { name: 'ENGINE_APP_ID' value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-ID/)' } - { - name: 'ENGINE_APP_SECRET' - value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-SECRET/)' - } { name: 'TENANT_ID' value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/TENANT-ID/)' diff --git a/deploy/modules/keyVault.bicep b/deploy/modules/keyVault.bicep index e2a4fdd..accc340 100644 --- a/deploy/modules/keyVault.bicep +++ b/deploy/modules/keyVault.bicep @@ -19,10 +19,6 @@ param uiAppId string @description('IPAM-Engine App Registration Client/App ID') param engineAppId string -@secure() -@description('IPAM-Engine App Registration Client Secret') -param engineAppSecret string - @description('Log Analytics Worskpace ID') param workspaceId string @@ -73,14 +69,6 @@ resource engineId 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { } } -resource engineSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'ENGINE-SECRET' - properties: { - value: engineAppSecret - } -} - resource appTenant 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { parent: keyVault name: 'TENANT-ID' diff --git a/engine/app/routers/common/helper.py b/engine/app/routers/common/helper.py index 580402e..4196b16 100644 --- a/engine/app/routers/common/helper.py +++ b/engine/app/routers/common/helper.py @@ -13,6 +13,7 @@ from azure.cosmos.aio import CosmosClient from azure.identity.aio import ( ClientSecretCredential, + ClientAssertionCredential, ManagedIdentityCredential, OnBehalfOfCredential, ) @@ -190,25 +191,48 @@ async def get_obo_token(assertion): async def get_client_credentials(): """Return a client-secret credential for the IPAM service principal (app-only auth).""" - credential = ClientSecretCredential( - tenant_id=globals.TENANT_ID, - client_id=globals.CLIENT_ID, - client_secret=globals.CLIENT_SECRET, - authority=globals.AUTHORITY_HOST - ) + # If CLIENT_SECRET is defined, use it for credentials. Otherwise, use the Managed Identity + if globals.CLIENT_SECRET: + credential = ClientSecretCredential( + tenant_id=globals.TENANT_ID, + client_id=globals.CLIENT_ID, + client_secret=globals.CLIENT_SECRET, + authority=globals.AUTHORITY_HOST + ) + else: + mi_token = await managed_identity_credential.get_token("api://AzureADTokenExchange") + + credential = ClientAssertionCredential( + tenant_id=globals.TENANT_ID, + client_id=globals.CLIENT_ID, + func=lambda: mi_token.token, + authority=globals.AUTHORITY_HOST + ) return credential async def get_obo_credentials(assertion): """Return an on-behalf-of credential built from the given user assertion (delegated auth).""" - credential = OnBehalfOfCredential( - tenant_id=globals.TENANT_ID, - client_id=globals.CLIENT_ID, - client_secret=globals.CLIENT_SECRET, - user_assertion=assertion, - authority=globals.AUTHORITY_HOST - ) + # If CLIENT_SECRET is defined, use it for credentials. Otherwise, use the Managed Identity + if globals.CLIENT_SECRET: + credential = OnBehalfOfCredential( + tenant_id=globals.TENANT_ID, + client_id=globals.CLIENT_ID, + client_secret=globals.CLIENT_SECRET, + user_assertion=assertion, + authority=globals.AUTHORITY_HOST + ) + else: + mi_token = await managed_identity_credential.get_token("api://AzureADTokenExchange") + + credential = OnBehalfOfCredential( + tenant_id=globals.TENANT_ID, + client_id=globals.CLIENT_ID, + client_assertion_func=lambda: mi_token.token, + user_assertion=assertion, + authority=globals.AUTHORITY_HOST + ) return credential diff --git a/engine/requirements.lock.txt b/engine/requirements.lock.txt index 63066ba..c616d27 100644 --- a/engine/requirements.lock.txt +++ b/engine/requirements.lock.txt @@ -19,7 +19,7 @@ azure-mgmt-datafactory==9.2.0 azure-mgmt-managementgroups==1.0.0 azure-mgmt-network==29.0.0 azure-mgmt-resource==25.0.0 -azure-mgmt-resource-subscriptions==1.0.0 +azure-mgmt-resource-subscriptions==1.0.0b1 azure-mgmt-resourcegraph==8.0.0 certifi==2025.8.3 cffi==2.0.0 diff --git a/migrate/modules/appService.bicep b/migrate/modules/appService.bicep index 2afd828..35e32b7 100644 --- a/migrate/modules/appService.bicep +++ b/migrate/modules/appService.bicep @@ -81,10 +81,6 @@ var appServiceSiteConfig = { name: 'ENGINE_APP_ID' value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-ID/)' } - { - name: 'ENGINE_APP_SECRET' - value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-SECRET/)' - } { name: 'TENANT_ID' value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/TENANT-ID/)'