Skip to content

Commit

Permalink
Use managed identity for SQL
Browse files Browse the repository at this point in the history
  • Loading branch information
twsouthwick committed Jun 4, 2024
1 parent 8cc0972 commit 8ecf26f
Show file tree
Hide file tree
Showing 14 changed files with 85 additions and 286 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ Select the subscription that will be used for the deployment:
azd env set AZURE_SUBSCRIPTION_ID $AZURE_SUBSCRIPTION_ID
```

Set your principal name:

```pwsh
azd env set AZURE_PRINCIPAL_NAME (Get-AzContext).Account.Id
```

Set the `AZURE_LOCATION` (Run `(Get-AzLocation).Location` to see a list of locations):

```pwsh
Expand Down
15 changes: 3 additions & 12 deletions developer-experience.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ The dev team uses Visual Studio and they integrate directly with Azure resources
Most configurations in the project are stored in Azure App Configuration with secrets saved into Azure Key Vault. To connect to these resources from a developer workstation you need to complete the following steps.

1. Add your identity to the Azure SQL resource
1. Set up front-end web app configuration
1. Set up back-end web app configuration

Expand Down Expand Up @@ -45,15 +44,7 @@ To support this workflow the following steps will store data in [User Secrets](h
Set-AzContext -SubscriptionId $AZURE_SUBSCRIPTION_ID
```

## 1. Add your identity to the Azure SQL resource

1. Run the following script to automate the process in docs [Configure and manage Microsoft Entra authentication with Azure SQL](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure?view=azuresql&tabs=azure-powershell)

```pwsh
./infra/scripts/devexperience/call-make-sql-account.ps1
```

## 2. Set up front-end web app configuration
## 1. Set up front-end web app configuration

1. Get the Azure App Configuration URI
```pwsh
Expand Down Expand Up @@ -83,7 +74,7 @@ To support this workflow the following steps will store data in [User Secrets](h
cd ../..
```
## 3. Set up back-end web app configuration
## 2. Set up back-end web app configuration
```pwsh
cd src/Relecloud.Web.CallCenter.Api
Expand All @@ -97,7 +88,7 @@ To support this workflow the following steps will store data in [User Secrets](h
dotnet user-secrets set "App:AppConfig:Uri" $appConfigurationUri
```
## 4. Launch the project with Visual Studio
## 3. Launch the project with Visual Studio
1. Open the project in Visual Studio
1. Configure the solution to start both the front-end and back-end web apps
Expand Down
13 changes: 4 additions & 9 deletions infra/core/database/create-sql-user-and-role.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ param managedIdentityId string
param principalId string

@description('The name of the user to create.')
param principalName string = ''

@allowed([ 'ServicePrincipal', 'User' ])
@description('The type of identity referenced by \'principalId\'.')
param principalType string = 'ServicePrincipal'
param principalName string

@description('The name of the SQL Database resource.')
param sqlDatabaseName string
Expand All @@ -48,7 +44,7 @@ param uniqueScriptId string = newGuid()
// ========================================================================

resource createSqlUserAndRole 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'createSqlUserAndRole-${principalId}'
name: 'sqlUserRole-${guid(principalId, databaseRoles, sqlServerName, sqlDatabaseName)}'
location: location
tags: tags
kind: 'AzurePowerShell'
Expand All @@ -67,9 +63,8 @@ resource createSqlUserAndRole 'Microsoft.Resources/deploymentScripts@2020-10-01'
'-SqlServerName \'${sqlServerName}\''
'-SqlDatabaseName \'${sqlDatabaseName}\''
'-ObjectId \'${principalId}\''
!empty(principalName) ? '-DisplayName \'${principalName}\'' : ''
principalType == 'ServicePrincipal' ? '-IsServicePrincipal' : ''
'-DatabaseRoles ${databaseRoles}'
'-DisplayName \'${principalName}\''
'-DatabaseRoles \'${databaseRoles}\''
], ' ')
scriptContent: loadTextContent('./scripts/create-sql-user-and-role.ps1')
}
Expand Down
42 changes: 14 additions & 28 deletions infra/core/database/scripts/create-sql-user-and-role.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@
The name of the SQL Database resource
.PARAMETER ObjectId
The Object (Principal) ID of the user to be added.
.PARAMETER DisplayName
The display name of the user to be added. This is optional. If not provided, the Get-AzADUser cmdlet
will be used to retrieve the display name.
.PARAMETER IsServicePrincipal
True if the ObjectId refers to a service principal rather than a user.
.PARAMETER DatabaseRoles
The comma-separated list of database roles that need to be assigned to the user.
#>
Expand All @@ -29,7 +24,6 @@ Param(
[string] $SqlDatabaseName,
[string] $ObjectId,
[string] $DisplayName,
[switch] $IsServicePrincipal = $false,
[string[]] $DatabaseRoles = @('db_datareader','db_datawriter')
)

Expand All @@ -44,37 +38,31 @@ function Resolve-Module($moduleName) {
Import-Module $moduleName
} else {
Write-Error "Module $moduleName not found"
Write-Host "###vso[task.complete result=Failed;]Failed"
[Environment]::exit(1)
}
}

function ConvertTo-Sid($applicationId) {
[System.Guid]$guid = [System.Guid]::Parse($applicationId)
foreach ($byte in $guid.ToByteArray()) {
$byteGuid += [System.String]::Format("{0:X2}", $byte)
}
return "0x" + $byteGuid
}

###
### MAIN SCRIPT
###
Resolve-Module -moduleName Az.Resources
Resolve-Module -moduleName SqlServer

# Get the SID for the ObjectId we are using
$Sid = ConvertTo-Sid -applicationId $ObjectId

# Construct the SQL to create the user.
$sqlList = [System.Collections.ArrayList]@()

$UserCreationOpt = if ($IsServicePrincipal) { "WITH sid = $($Sid), type = E" } else { "FROM EXTERNAL PROVIDER" }
# Use a SID representation of the object id so that SQL doesn't have to be connected to Entra
$CreateUserSql = @"
IF NOT EXISTS (
SELECT * FROM sys.database_principals WHERE name = N'$($DisplayName)'
)
CREATE USER [$($DisplayName)] $($UserCreationOpt);
-- Replace the two variables with the MS Entra user alias and object ID
declare @username sysname = $($DisplayName);
declare @objectId uniqueidentifier = '$($ObjectId)';
-- convert the guid to the right type
declare @castObjectId nvarchar(max) = CONVERT(varchar(max), convert (varbinary(16), @objectId), 1);
-- Construct command: CREATE USER [@username] WITH SID = @castObjectId, TYPE = E;
declare @cmd nvarchar(max) = N'CREATE USER [' + @username + '] WITH SID = ' + @castObjectId + ', TYPE = E;'
EXEC (@cmd)
"@
$sqlList.Add($CreateUserSql) | Out-Null

Expand All @@ -92,17 +80,15 @@ ALTER ROLE $($role) ADD MEMBER [$($DisplayName)];
$sqlList.Add($GrantRoleSql) | Out-Null
}

$token = (Get-AzAccessToken -ResourceUrl https://database.windows.net/).Token

# Execute the SQL Command on Azure SQL.
foreach ($sqlcmd in $sqlList) {
try {
$sqlcmd | Write-Output
$token = (Get-AzAccessToken -ResourceUrl https://database.windows.net/).Token
Invoke-SqlCmd -ServerInstance "$SqlServerName.database.windows.net" -Database $SqlDatabaseName -AccessToken $token -Query $sqlcmd -ErrorAction 'Stop' -StatisticsVariable 'stats'
$stats | ConvertTo-Json -Depth 10 | Write-Output
} catch {
Write-Error $_.Exception.Message
Write-Host "###vso[task.complete result=Failed;]Failed"
$_.Exception.Message | Write-Error
[Environment]::exit(1)
}
}

33 changes: 32 additions & 1 deletion infra/core/database/sql-database.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,22 @@ type DiagnosticSettings = {
enableMetrics: bool
}

// From: infra/types/UserIdentity.bicep
@description('Type describing a user identity.')
type UserIdentity = {
@description('The ID of the user')
principalId: string

@description('The name of the user')
principalName: string
}

// From: infra/types/PrivateEndpointSettings.bicep
@description('Type describing the private endpoint settings.')
type PrivateEndpointSettings = {
@description('The name of the resource group to hold the Private DNS Zone. By default, this uses the same resource group as the resource.')
dnsResourceGroupName: string

@description('The name of the private endpoint resource. By default, this uses a prefix of \'pe-\' followed by the name of the resource.')
name: string

Expand Down Expand Up @@ -82,6 +92,9 @@ param privateEndpointSettings PrivateEndpointSettings?
@description('The service tier to use for the database.')
param sku string = 'Basic'

param users UserIdentity[] = []
param managedIdentityName string

@description('If true, enable availability zone redundancy.')
param zoneRedundant bool = false

Expand All @@ -107,6 +120,10 @@ var logCategories = [
// AZURE RESOURCES
// ========================================================================

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
name: managedIdentityName
}

resource sqlServer 'Microsoft.Sql/servers@2021-11-01' existing = {
name: sqlServerName
}
Expand Down Expand Up @@ -168,6 +185,20 @@ resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview'
}
}

module sqluser 'create-sql-user-and-role.bicep' = [for user in users: {
name: 'sqluser-${guid(location, user.principalId, user.principalName, name, sqlServer.name)}'
params: {
managedIdentityId: managedIdentity.id
principalId: user.principalId
principalName: user.principalName
sqlDatabaseName: name
location: location
sqlServerName: sqlServer.name
databaseRoles: 'db_owner'
}
dependsOn: [ sqlDatabase ]
}]

// ========================================================================
// OUTPUTS
// ========================================================================
Expand Down
13 changes: 1 addition & 12 deletions infra/core/database/sql-server.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,6 @@ param enablePublicNetworkAccess bool = true
@description('The firewall rules to install on the Key Vault.')
param firewallRules FirewallRules?

@secure()
@minLength(8)
@description('The password for the administrator account on the SQL Server.')
param sqlAdministratorPassword string = newGuid()

@minLength(8)
@description('The username for the administrator account on the SQL Server.')
param sqlAdministratorUsername string = 'adminuser'

// ========================================================================
// VARIABLES
// ========================================================================
Expand All @@ -100,10 +91,8 @@ resource sqlServer 'Microsoft.Sql/servers@2021-11-01' = {
location: location
tags: tags
properties: {
administratorLogin: sqlAdministratorUsername
administratorLoginPassword: sqlAdministratorPassword
administrators: {
azureADOnlyAuthentication: false
azureADOnlyAuthentication: true
login: managedIdentity.name
principalType: 'User'
sid: managedIdentity.properties.principalId
Expand Down
13 changes: 0 additions & 13 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,6 @@ param principalId string = ''
@description('The type of the principal specified in \'principalId\'')
param principalType string = 'ServicePrincipal'

/*
** Passwords - specify these!
*/
@secure()
@minLength(8)
@description('The password for the SQL administrator account. This will be used for the jump box, SQL server, and anywhere else a password is needed for creating a resource.')
param databasePassword string

@secure()
@minLength(12)
@description('The password for the jump box administrator account.')
Expand Down Expand Up @@ -442,8 +434,6 @@ module application './modules/application-resources.bicep' = {
frontDoorSettings: frontdoor.outputs.settings

// Settings
administratorUsername: administratorUsername
databasePassword: databasePassword
clientIpAddress: clientIpAddress
useCommonAppServicePlan: willDeployCommonAppServicePlan
}
Expand All @@ -468,8 +458,6 @@ module application2 './modules/application-resources.bicep' = if (isMultiLocati
frontDoorSettings: frontdoor.outputs.settings

// Settings
administratorUsername: administratorUsername
databasePassword: databasePassword
clientIpAddress: clientIpAddress
useCommonAppServicePlan: willDeployCommonAppServicePlan
}
Expand All @@ -488,7 +476,6 @@ module applicationPostConfiguration './modules/application-post-config.bicep' =
deploymentSettings: primaryDeploymentSettings
administratorPassword: jumpboxAdministratorPassword
administratorUsername: administratorUsername
databasePassword: databasePassword
keyVaultName: isNetworkIsolated? hubNetwork.outputs.key_vault_name : application.outputs.key_vault_name
kvResourceGroupName: isNetworkIsolated? resourceGroups.outputs.hub_resource_group_name : resourceGroups.outputs.application_resource_group_name
readerIdentities: union(application.outputs.service_managed_identities, defaultDeploymentSettings.isMultiLocationDeployment ? application2.outputs.service_managed_identities : [])
Expand Down
6 changes: 3 additions & 3 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
"clientIpAddress": {
"value": "${AZD_IP_ADDRESS}"
},
"databasePassword": {
"value": "$(secretOrRandomPassword ${AZURE_OPS_VAULT_NAME} Application--SqlAdministratorPassword)"
},
"differentiator": {
"value": "${AZURE_CI_DIFFERENTIATOR=none}"
},
Expand Down Expand Up @@ -41,6 +38,9 @@
"principalId": {
"value": "${AZURE_PRINCIPAL_ID}"
},
"principalName": {
"value": "${AZURE_PRINCIPAL_NAME}"
},
"principalType": {
"value": "${AZURE_PRINCIPAL_TYPE=User}"
},
Expand Down
19 changes: 1 addition & 18 deletions infra/modules/application-post-config.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,13 @@ param deploymentSettings DeploymentSettings
*/
@secure()
@minLength(12)
@description('The password for the administrator account. This will be used for the jump box, SQL server, and anywhere else a password is needed for creating a resource.')
@description('The password for the administrator account. This will be used for the jump box and anywhere else a password is needed for creating a resource.')
param administratorPassword string = newGuid()

@minLength(8)
@description('The username for the administrator account on the jump box.')
param administratorUsername string = 'adminuser'

@secure()
@minLength(8)
@description('The password for the administrator account on the SQL Server.')
param databasePassword string

@description('The resource names for the resources to be created.')
param resourceNames object

Expand Down Expand Up @@ -154,18 +149,6 @@ module writeJumpBoxCredentialsToKeyVault '../core/security/key-vault-secrets.bic
}
}

module writeSqlAdminInfoToKeyVault '../core/security/key-vault-secrets.bicep' = {
name: 'write-sql-admin-info-to-keyvault'
scope: existingKvResourceGroup
params: {
name: existingKeyVault.name
secrets: [
{ key: 'Application--SqlAdministratorUsername', value: administratorUsername }
{ key: 'Application--SqlAdministratorPassword', value: databasePassword }
]
}
}

// ======================================================================== //
// Microsoft Entra Application Registration placeholders
// ======================================================================== //
Expand Down
Loading

0 comments on commit 8ecf26f

Please sign in to comment.