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 May 9, 2024
1 parent 4143910 commit b85abdf
Show file tree
Hide file tree
Showing 10 changed files with 42 additions and 275 deletions.
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
19 changes: 5 additions & 14 deletions infra/core/database/create-sql-user-and-role.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,8 @@ param databaseRoles string = 'db_datareader'
@description('The ID of the managed identity to be used to run the script.')
param managedIdentityId string

@description('The principal (or object) ID of the user to create.')
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'
@description('The object ID of the user to create.')
param objectId string

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

resource createSqlUserAndRole 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'createSqlUserAndRole-${principalId}'
name: 'sqlUserRole-${guid(objectId, databaseRoles, sqlServerName, sqlDatabaseName)}'
location: location
tags: tags
kind: 'AzurePowerShell'
Expand All @@ -66,10 +59,8 @@ resource createSqlUserAndRole 'Microsoft.Resources/deploymentScripts@2020-10-01'
arguments: join([
'-SqlServerName \'${sqlServerName}\''
'-SqlDatabaseName \'${sqlDatabaseName}\''
'-ObjectId \'${principalId}\''
!empty(principalName) ? '-DisplayName \'${principalName}\'' : ''
principalType == 'ServicePrincipal' ? '-IsServicePrincipal' : ''
'-DatabaseRoles ${databaseRoles}'
'-ObjectId \'${objectId}\''
'-DatabaseRoles \'${databaseRoles}\''
], ' ')
scriptContent: loadTextContent('./scripts/create-sql-user-and-role.ps1')
}
Expand Down
24 changes: 9 additions & 15 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,7 +38,6 @@ 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)
}
}
Expand All @@ -60,20 +53,22 @@ function ConvertTo-Sid($applicationId) {
###
### 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
# This is just for a display name in SQL and since we just have the object id, we'll prepend an identifier
$DisplayName = "user" + $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
$Sid = ConvertTo-Sid -applicationId $ObjectId
$CreateUserSql = @"
IF NOT EXISTS (
SELECT * FROM sys.database_principals WHERE name = N'$($DisplayName)'
)
CREATE USER [$($DisplayName)] $($UserCreationOpt);
CREATE USER [$($DisplayName)] WITH sid = $Sid, type = E;
"@
$sqlList.Add($CreateUserSql) | Out-Null
Expand All @@ -92,17 +87,16 @@ 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)
}
}

22 changes: 21 additions & 1 deletion infra/core/database/sql-database.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type DiagnosticSettings = {
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 +82,9 @@ param privateEndpointSettings PrivateEndpointSettings?
@description('The service tier to use for the database.')
param sku string = 'Basic'

param users string[] = []
param managedIdentityName string

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

Expand All @@ -107,6 +110,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 +175,19 @@ 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, name, sqlServer.name)}'
params: {
managedIdentityId: managedIdentity.id
objectId: user
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
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
12 changes: 2 additions & 10 deletions infra/modules/application-resources.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,6 @@ param frontDoorSettings FrontDoorSettings
/*
** Settings
*/
@secure()
@minLength(8)
@description('The password for the administrator account on the SQL Server.')
param databasePassword string

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

@description('The IP address of the current system. This is used to set up the firewall for Key Vault and SQL Server if in development mode.')
param clientIpAddress string = ''
Expand Down Expand Up @@ -293,8 +285,6 @@ module sqlServer '../core/database/sql-server.bicep' = if (createSqlServer) {
} : null
diagnosticSettings: diagnosticSettings
enablePublicNetworkAccess: !deploymentSettings.isNetworkIsolated
sqlAdministratorPassword: databasePassword
sqlAdministratorUsername: administratorUsername
}
}

Expand All @@ -321,6 +311,8 @@ module sqlDatabase '../core/database/sql-database.bicep' = {
} : null
sku: deploymentSettings.isProduction ? 'Premium' : 'Standard'
zoneRedundant: deploymentSettings.isProduction
users: deploymentSettings.principalId != null ? [ deploymentSettings.principalId ] : []
managedIdentityName: ownerManagedIdentity.outputs.name
}
}

Expand Down
48 changes: 0 additions & 48 deletions infra/scripts/devexperience/call-make-sql-account.ps1

This file was deleted.

Loading

0 comments on commit b85abdf

Please sign in to comment.