Pamela Fox commited on
Commit
f24be86
·
1 Parent(s): cd77237

Port to passwordless

Browse files
azure.yaml CHANGED
@@ -8,3 +8,10 @@ services:
8
  project: ./src
9
  language: py
10
  host: appservice
 
 
 
 
 
 
 
 
8
  project: ./src
9
  language: py
10
  host: appservice
11
+ hooks:
12
+ postprovision:
13
+ posix:
14
+ shell: sh
15
+ run: ./scripts/setup_postgres_azurerole.sh
16
+ interactive: true
17
+ continueOnError: false
infra/core/database/postgresql/flexibleserver.bicep CHANGED
@@ -4,9 +4,32 @@ param tags object = {}
4
 
5
  param sku object
6
  param storage object
7
- param administratorLogin string
 
 
 
 
 
 
 
8
  @secure()
9
- param administratorLoginPassword string
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  param databaseNames array = []
11
  param allowAzureIPsFirewall bool = false
12
  param allowAllIPsFirewall bool = false
@@ -15,50 +38,93 @@ param allowedSingleIPs array = []
15
  // PostgreSQL version
16
  param version string
17
 
18
- // Latest official version 2022-12-01 does not have Bicep types available
 
 
 
 
 
 
 
 
 
 
 
 
19
  resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {
20
  location: location
21
  tags: tags
22
  name: name
23
  sku: sku
24
- properties: {
25
  version: version
26
- administratorLogin: administratorLogin
27
- administratorLoginPassword: administratorLoginPassword
28
  storage: storage
29
  highAvailability: {
30
  mode: 'Disabled'
31
  }
32
- }
33
 
34
  resource database 'databases' = [for name in databaseNames: {
35
  name: name
36
  }]
 
37
 
38
- resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) {
39
- name: 'allow-all-IPs'
40
- properties: {
41
- startIpAddress: '0.0.0.0'
42
- endIpAddress: '255.255.255.255'
43
- }
 
44
  }
 
45
 
46
- resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) {
47
- name: 'allow-all-azure-internal-IPs'
48
- properties: {
49
- startIpAddress: '0.0.0.0'
50
- endIpAddress: '0.0.0.0'
51
- }
 
52
  }
 
53
 
54
- resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: {
55
- name: 'allow-single-${replace(ip, '.', '')}'
56
- properties: {
57
- startIpAddress: ip
58
- endIpAddress: ip
59
- }
60
- }]
 
 
 
61
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
 
64
- output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  param sku object
6
  param storage object
7
+
8
+ @allowed([
9
+ 'Password'
10
+ 'EntraOnly'
11
+ ])
12
+ param authType string = 'Password'
13
+
14
+ param administratorLogin string = ''
15
  @secure()
16
+ param administratorLoginPassword string = ''
17
+
18
+ @description('Entra admin role name')
19
+ param entraAdministratorName string = ''
20
+
21
+ @description('Entra admin role object ID (in Entra)')
22
+ param entraAdministratorObjectId string = ''
23
+
24
+ @description('Entra admin user type')
25
+ @allowed([
26
+ 'User'
27
+ 'Group'
28
+ 'ServicePrincipal'
29
+ ])
30
+ param entraAdministratorType string = 'User'
31
+
32
+
33
  param databaseNames array = []
34
  param allowAzureIPsFirewall bool = false
35
  param allowAllIPsFirewall bool = false
 
38
  // PostgreSQL version
39
  param version string
40
 
41
+ var authProperties = authType == 'Password' ? {
42
+ administratorLogin: administratorLogin
43
+ administratorLoginPassword: administratorLoginPassword
44
+ authConfig: {
45
+ passwordAuth: 'Enabled'
46
+ }
47
+ } : {
48
+ authConfig: {
49
+ activeDirectoryAuth: 'Enabled'
50
+ passwordAuth: 'Disabled'
51
+ }
52
+ }
53
+
54
  resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {
55
  location: location
56
  tags: tags
57
  name: name
58
  sku: sku
59
+ properties: union(authProperties, {
60
  version: version
 
 
61
  storage: storage
62
  highAvailability: {
63
  mode: 'Disabled'
64
  }
65
+ })
66
 
67
  resource database 'databases' = [for name in databaseNames: {
68
  name: name
69
  }]
70
+ }
71
 
72
+ // This must be done separately due to conflicts with the Entra setup
73
+ resource firewall_all 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = if (allowAllIPsFirewall) {
74
+ parent: postgresServer
75
+ name: 'allow-all-IPs'
76
+ properties: {
77
+ startIpAddress: '0.0.0.0'
78
+ endIpAddress: '255.255.255.255'
79
  }
80
+ }
81
 
82
+ // This must be done separately due to conflicts with the Entra setup
83
+ resource firewall_azure 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = if (allowAzureIPsFirewall) {
84
+ parent: postgresServer
85
+ name: 'allow-all-azure-internal-IPs'
86
+ properties: {
87
+ startIpAddress: '0.0.0.0'
88
+ endIpAddress: '0.0.0.0'
89
  }
90
+ }
91
 
92
+ @batchSize(1)
93
+ // This must be done separately due to conflicts with the Entra setup
94
+ resource firewall_single 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = [for ip in allowedSingleIPs: {
95
+ parent: postgresServer
96
+ name: 'allow-single-${replace(ip, '.', '')}'
97
+ properties: {
98
+ startIpAddress: ip
99
+ endIpAddress: ip
100
+ }
101
+ }]
102
 
103
+ // This must be created *after* the server is created - it cannot be a nested child resource
104
+ resource addAddUser 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2023-03-01-preview' = {
105
+ parent: postgresServer
106
+ name: entraAdministratorObjectId
107
+ properties: {
108
+ tenantId: subscription().tenantId
109
+ principalType: entraAdministratorType
110
+ principalName: entraAdministratorName
111
+ }
112
+ // This is a workaround for a bug in the API that requires the parent to be fully resolved
113
+ dependsOn: [postgresServer, firewall_all, firewall_azure]
114
  }
115
 
116
+ // Workaround issue https://github.com/Azure/bicep-types-az/issues/1507
117
+ resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
118
+ name: 'azure.extensions'
119
+ parent: postgresServer
120
+ properties: {
121
+ value: 'vector'
122
+ source: 'user-override'
123
+ }
124
+ dependsOn: [
125
+ addAddUser, firewall_all, firewall_azure, firewall_single
126
+ ]
127
+ }
128
+
129
+
130
+ output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName
infra/core/security/keyvault.bicep CHANGED
@@ -2,8 +2,6 @@ param name string
2
  param location string = resourceGroup().location
3
  param tags object = {}
4
 
5
- param principalId string = ''
6
-
7
  resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
8
  name: name
9
  location: location
@@ -11,13 +9,7 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
11
  properties: {
12
  tenantId: subscription().tenantId
13
  sku: { family: 'A', name: 'standard' }
14
- accessPolicies: !empty(principalId) ? [
15
- {
16
- objectId: principalId
17
- permissions: { secrets: [ 'get', 'list' ] }
18
- tenantId: subscription().tenantId
19
- }
20
- ] : []
21
  }
22
  }
23
 
 
2
  param location string = resourceGroup().location
3
  param tags object = {}
4
 
 
 
5
  resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
6
  name: name
7
  location: location
 
9
  properties: {
10
  tenantId: subscription().tenantId
11
  sku: { family: 'A', name: 'standard' }
12
+ enableRbacAuthorization: true
 
 
 
 
 
 
13
  }
14
  }
15
 
infra/core/security/role.bicep ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ metadata description = 'Creates a role assignment for a service principal.'
2
+ param principalId string
3
+
4
+ @allowed([
5
+ 'Device'
6
+ 'ForeignGroup'
7
+ 'Group'
8
+ 'ServicePrincipal'
9
+ 'User'
10
+ ])
11
+ param principalType string = 'ServicePrincipal'
12
+ param roleDefinitionId string
13
+
14
+ resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
15
+ name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId)
16
+ properties: {
17
+ principalId: principalId
18
+ principalType: principalType
19
+ roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
20
+ }
21
+ }
infra/main.bicep CHANGED
@@ -9,9 +9,19 @@ param name string
9
  @description('Primary location for all resources')
10
  param location string
11
 
12
- @secure()
13
- @description('PostGreSQL Server administrator password')
14
- param postgresAdminPassword string
 
 
 
 
 
 
 
 
 
 
15
 
16
  @description('Id of the user or app to assign application roles')
17
  param principalId string = ''
@@ -20,6 +30,9 @@ param principalId string = ''
20
  @description('Django SECRET_KEY for cryptographic signing')
21
  param djangoSecretKey string
22
 
 
 
 
23
  var resourceToken = toLower(uniqueString(subscription().id, name, location))
24
  var tags = { 'azd-env-name': name }
25
 
@@ -32,7 +45,6 @@ resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
32
  var prefix = '${name}-${resourceToken}'
33
 
34
  var postgresServerName = '${prefix}-postgresql'
35
- var postgresAdminUser = 'admin${uniqueString(resourceGroup.id)}'
36
  var postgresDatabaseName = 'django'
37
 
38
  module postgresServer 'core/database/postgresql/flexibleserver.bicep' = {
@@ -50,18 +62,22 @@ module postgresServer 'core/database/postgresql/flexibleserver.bicep' = {
50
  storageSizeGB: 32
51
  }
52
  version: '14'
53
- administratorLogin: postgresAdminUser
54
- administratorLoginPassword: postgresAdminPassword
 
 
55
  databaseNames: [ postgresDatabaseName ]
56
  allowAzureIPsFirewall: true
 
57
  }
58
  }
59
 
 
60
  module web 'core/host/appservice.bicep' = {
61
  name: 'appservice'
62
  scope: resourceGroup
63
  params: {
64
- name: '${prefix}-appservice'
65
  location: location
66
  tags: union(tags, { 'azd-service-name': 'web' })
67
  appServicePlanId: appServicePlan.outputs.id
@@ -74,10 +90,9 @@ module web 'core/host/appservice.bicep' = {
74
  appSettings: {
75
  ADMIN_URL: 'admin${uniqueString(appServicePlan.outputs.id)}'
76
  DBENGINE: 'django.db.backends.postgresql'
77
- DBHOST: '${postgresServerName}.postgres.database.azure.com'
78
  DBNAME: postgresDatabaseName
79
- DBUSER: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminUser)'
80
- DBPASS: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminPassword)'
81
  DBSSL: 'require'
82
  STATIC_BACKEND: 'whitenoise.storage.CompressedManifestStaticFilesStorage'
83
  SECRET_KEY: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=djangoSecretKey)'
@@ -116,7 +131,26 @@ module keyVault './core/security/keyvault.bicep' = {
116
  name: '${take(replace(prefix, '-', ''), 17)}-vault'
117
  location: location
118
  tags: tags
 
 
 
 
 
 
 
119
  principalId: principalId
 
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
  }
122
 
@@ -125,14 +159,6 @@ var secrets = [
125
  name: 'djangoSecretKey'
126
  value: djangoSecretKey
127
  }
128
- {
129
- name: 'postgresAdminUser'
130
- value: postgresAdminUser
131
- }
132
- {
133
- name: 'postgresAdminPassword'
134
- value: postgresAdminPassword
135
- }
136
  ]
137
 
138
  @batchSize(1)
@@ -146,6 +172,8 @@ module keyVaultSecrets './core/security/keyvault-secret.bicep' = [for secret in
146
  }
147
  }]
148
 
 
 
149
  module logAnalyticsWorkspace 'core/monitor/loganalytics.bicep' = {
150
  name: 'loganalytics'
151
  scope: resourceGroup
@@ -156,6 +184,12 @@ module logAnalyticsWorkspace 'core/monitor/loganalytics.bicep' = {
156
  }
157
  }
158
 
 
159
  output WEB_URI string = 'https://${web.outputs.uri}'
 
 
160
  output AZURE_LOCATION string = location
161
- output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name
 
 
 
 
9
  @description('Primary location for all resources')
10
  param location string
11
 
12
+ @description('Entra admin role name')
13
+ param postgresEntraAdministratorName string
14
+
15
+ @description('Entra admin role object ID (in Entra)')
16
+ param postgresEntraAdministratorObjectId string
17
+
18
+ @description('Entra admin user type')
19
+ @allowed([
20
+ 'User'
21
+ 'Group'
22
+ 'ServicePrincipal'
23
+ ])
24
+ param postgresEntraAdministratorType string = 'User'
25
 
26
  @description('Id of the user or app to assign application roles')
27
  param principalId string = ''
 
30
  @description('Django SECRET_KEY for cryptographic signing')
31
  param djangoSecretKey string
32
 
33
+ @description('Running on GitHub Actions?')
34
+ param runningOnGh bool = false
35
+
36
  var resourceToken = toLower(uniqueString(subscription().id, name, location))
37
  var tags = { 'azd-env-name': name }
38
 
 
45
  var prefix = '${name}-${resourceToken}'
46
 
47
  var postgresServerName = '${prefix}-postgresql'
 
48
  var postgresDatabaseName = 'django'
49
 
50
  module postgresServer 'core/database/postgresql/flexibleserver.bicep' = {
 
62
  storageSizeGB: 32
63
  }
64
  version: '14'
65
+ authType: 'EntraOnly'
66
+ entraAdministratorName: postgresEntraAdministratorName
67
+ entraAdministratorObjectId: postgresEntraAdministratorObjectId
68
+ entraAdministratorType: postgresEntraAdministratorType
69
  databaseNames: [ postgresDatabaseName ]
70
  allowAzureIPsFirewall: true
71
+ allowAllIPsFirewall: true // Necessary for post-provision script, can be disabled after
72
  }
73
  }
74
 
75
+ var webAppName = '${prefix}-app-service'
76
  module web 'core/host/appservice.bicep' = {
77
  name: 'appservice'
78
  scope: resourceGroup
79
  params: {
80
+ name: webAppName
81
  location: location
82
  tags: union(tags, { 'azd-service-name': 'web' })
83
  appServicePlanId: appServicePlan.outputs.id
 
90
  appSettings: {
91
  ADMIN_URL: 'admin${uniqueString(appServicePlan.outputs.id)}'
92
  DBENGINE: 'django.db.backends.postgresql'
93
+ DBHOST: '${postgresServerName}.postgres.database.azure.com' // todo replace with ouput
94
  DBNAME: postgresDatabaseName
95
+ DBUSER: webAppName
 
96
  DBSSL: 'require'
97
  STATIC_BACKEND: 'whitenoise.storage.CompressedManifestStaticFilesStorage'
98
  SECRET_KEY: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=djangoSecretKey)'
 
131
  name: '${take(replace(prefix, '-', ''), 17)}-vault'
132
  location: location
133
  tags: tags
134
+ }
135
+ }
136
+
137
+ module userKeyVaultAccess 'core/security/role.bicep' = {
138
+ name: 'user-keyvault-access'
139
+ scope: resourceGroup
140
+ params: {
141
  principalId: principalId
142
+ principalType: runningOnGh ? 'ServicePrincipal' : 'User'
143
+ roleDefinitionId: '00482a5a-887f-4fb3-b363-3b7fe8e74483'
144
+ }
145
+ }
146
+
147
+ module backendKeyVaultAccess 'core/security/role.bicep' = {
148
+ name: 'backend-keyvault-access'
149
+ scope: resourceGroup
150
+ params: {
151
+ principalId: web.outputs.identityPrincipalId
152
+ principalType: 'ServicePrincipal'
153
+ roleDefinitionId: '00482a5a-887f-4fb3-b363-3b7fe8e74483'
154
  }
155
  }
156
 
 
159
  name: 'djangoSecretKey'
160
  value: djangoSecretKey
161
  }
 
 
 
 
 
 
 
 
162
  ]
163
 
164
  @batchSize(1)
 
172
  }
173
  }]
174
 
175
+
176
+
177
  module logAnalyticsWorkspace 'core/monitor/loganalytics.bicep' = {
178
  name: 'loganalytics'
179
  scope: resourceGroup
 
184
  }
185
  }
186
 
187
+ output WEB_APP_NAME string = webAppName
188
  output WEB_URI string = 'https://${web.outputs.uri}'
189
+ output SERVICE_WEB_IDENTITY_NAME string = webAppName
190
+
191
  output AZURE_LOCATION string = location
192
+ output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name
193
+
194
+ output POSTGRES_HOST string = postgresServer.outputs.POSTGRES_DOMAIN_NAME
195
+ output POSTGRES_USERNAME string = postgresEntraAdministratorName
infra/main.parameters.json CHANGED
@@ -11,11 +11,20 @@
11
  "principalId": {
12
  "value": "${AZURE_PRINCIPAL_ID}"
13
  },
14
- "postgresAdminPassword": {
15
- "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} postgresAdminPassword)"
16
  },
17
  "djangoSecretKey": {
18
  "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} djangoSecretKey)"
 
 
 
 
 
 
 
 
 
19
  }
20
  }
21
  }
 
11
  "principalId": {
12
  "value": "${AZURE_PRINCIPAL_ID}"
13
  },
14
+ "runningOnGh": {
15
+ "value": "${GITHUB_ACTIONS}"
16
  },
17
  "djangoSecretKey": {
18
  "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} djangoSecretKey)"
19
+ },
20
+ "postgresEntraAdministratorName": {
21
+ "value": "useradmin"
22
+ },
23
+ "postgresEntraAdministratorObjectId": {
24
+ "value": "${AZURE_PRINCIPAL_ID}"
25
+ },
26
+ "postgresEntraAdministratorType": {
27
+ "value": "User"
28
  }
29
  }
30
  }
scripts/load_python_env.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+
3
+ echo 'Creating Python virtual environment in ".venv"...'
4
+ python3 -m venv .venv
5
+
6
+ echo 'Installing dependencies from "scripts/requirements.txt" into virtual environment (in quiet mode)...'
7
+ .venv/bin/python -m pip --quiet --disable-pip-version-check install -r scripts/requirements.txt
scripts/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ psycopg2==2.9.9
2
+ azure-identity==1.16.0
scripts/setup_postgres_azurerole.ps1 ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ . ./scripts/load_python_env.ps1
2
+
3
+ $POSTGRES_HOST = ((azd env get-values | Select-String -Pattern "POSTGRES_HOST") -replace '^POSTGRES_HOST=', '')
4
+ $POSTGRES_USERNAME = ((azd env get-values | Select-String -Pattern "POSTGRES_USERNAME") -replace '^POSTGRES_USERNAME=', '')
5
+ $APP_IDENTITY_NAME = ((azd env get-values | Select-String -Pattern "SERVICE_WEB_IDENTITY_NAME") -replace '^SERVICE_WEB_IDENTITY_NAME=', '')
6
+
7
+ if ([string]::IsNullOrEmpty($POSTGRES_HOST) -or [string]::IsNullOrEmpty($POSTGRES_USERNAME) -or [string]::IsNullOrEmpty($APP_IDENTITY_NAME)) {
8
+ Write-Host "Can't find POSTGRES_HOST, POSTGRES_USERNAME, and SERVICE_WEB_IDENTITY_NAME environment variables. Make sure you run azd up first."
9
+ exit 1
10
+ }
11
+
12
+ $venvPythonPath = "./.venv/scripts/python.exe"
13
+ if (Test-Path -Path "/usr") {
14
+ # fallback to Linux venv path
15
+ $venvPythonPath = "./.venv/bin/python"
16
+ }
17
+
18
+ Start-Process -FilePath $venvPythonPath -ArgumentList "./src/setup_postgres_azurerole.py", "--host", $POSTGRES_HOST, "--username", $POSTGRES_USERNAME, "--app-identity-name", $APP_IDENTITY_NAME -Wait -NoNewWindow
scripts/setup_postgres_azurerole.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ POSTGRES_HOST=$(azd env get-values | grep POSTGRES_HOST | sed 's/="/=/' | sed 's/"$//' | sed 's/^POSTGRES_HOST=//')
2
+ POSTGRES_USERNAME=$(azd env get-values | grep POSTGRES_USERNAME | sed 's/="/=/' | sed 's/"$//' | sed 's/^POSTGRES_USERNAME=//')
3
+ APP_IDENTITY_NAME=$(azd env get-values | grep SERVICE_WEB_IDENTITY_NAME | sed 's/="/=/' | sed 's/"$//' | sed 's/^SERVICE_WEB_IDENTITY_NAME=//')
4
+
5
+ if [ -z "$POSTGRES_HOST" ] || [ -z "$POSTGRES_USERNAME" ] || [ -z "$APP_IDENTITY_NAME" ]; then
6
+ echo "Can't find POSTGRES_HOST, POSTGRES_USERNAME, and SERVICE_WEB_IDENTITY_NAME environment variables. Make sure you run azd up first."
7
+ exit 1
8
+ fi
9
+
10
+ . ./scripts/load_python_env.sh
11
+
12
+ ./.venv/bin/python ./src/setup_postgres_azurerole.py --host $POSTGRES_HOST --username $POSTGRES_USERNAME --app-identity-name $APP_IDENTITY_NAME
src/quizsite/postgresql/__init__.py ADDED
File without changes
src/quizsite/postgresql/base.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from azure.identity import DefaultAzureCredential
2
+ from django.db.backends.postgresql import base
3
+
4
+
5
+ class DatabaseWrapper(base.DatabaseWrapper):
6
+ def get_connection_params(self):
7
+ params = super().get_connection_params()
8
+ if params.get("host", "").endswith(".database.azure.com"):
9
+ azure_credential = DefaultAzureCredential()
10
+ dbpass = azure_credential.get_token("https://ossrdbms-aad.database.windows.net/.default").token
11
+ params["password"] = dbpass
12
+ return params
src/quizsite/settings.py CHANGED
@@ -108,12 +108,13 @@ WSGI_APPLICATION = "quizsite.wsgi.application"
108
 
109
  DATABASES = {
110
  "default": {
111
- "ENGINE": env("DBENGINE"),
112
  "NAME": env("DBNAME"),
113
  "HOST": env("DBHOST"),
114
  "USER": env("DBUSER"),
115
- "PASSWORD": env("DBPASS"),
116
  "OPTIONS": {"sslmode": env("DBSSL")},
 
117
  }
118
  }
119
 
 
108
 
109
  DATABASES = {
110
  "default": {
111
+ "ENGINE": "quizsite.postgresql",
112
  "NAME": env("DBNAME"),
113
  "HOST": env("DBHOST"),
114
  "USER": env("DBUSER"),
115
+ "PASSWORD": env("DBPASS", default="PASSWORD_WILL_BE_SET_LATER"),
116
  "OPTIONS": {"sslmode": env("DBSSL")},
117
+ "CONN_MAX_AGE": 60 * 60 * 6, # 6 hours
118
  }
119
  }
120
 
src/requirements.txt CHANGED
@@ -3,3 +3,4 @@ psycopg2==2.9.9
3
  python-dotenv==1.0.1
4
  whitenoise[brotli]==6.6.0
5
  django-environ==0.11.2
 
 
3
  python-dotenv==1.0.1
4
  whitenoise[brotli]==6.6.0
5
  django-environ==0.11.2
6
+ azure-identity==1.16.0
src/setup_postgres_azurerole.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import logging
3
+
4
+ import psycopg2
5
+ from azure.identity import DefaultAzureCredential
6
+
7
+ logger = logging.getLogger("scripts")
8
+
9
+
10
+ def assign_role_for_webapp(postgres_host, postgres_username, app_identity_name):
11
+ if not postgres_host.endswith(".database.azure.com"):
12
+ logger.info("This script is intended to be used with Azure Database for PostgreSQL.")
13
+ logger.info("Please set the environment variable DBHOST to the Azure Database for PostgreSQL server hostname.")
14
+ return
15
+
16
+ logger.info("Authenticating to Azure Database for PostgreSQL using Azure Identity...")
17
+ azure_credential = DefaultAzureCredential()
18
+ token = azure_credential.get_token("https://ossrdbms-aad.database.windows.net/.default")
19
+ conn = psycopg2.connect(
20
+ database="postgres", # You must connect to postgres database when assigning roles
21
+ user=postgres_username,
22
+ password=token.token,
23
+ host=postgres_host,
24
+ sslmode="require",
25
+ )
26
+
27
+ conn.autocommit = True
28
+ cur = conn.cursor()
29
+
30
+ cur.execute(f"select * from pgaadauth_list_principals(false) WHERE rolname = '{app_identity_name}'")
31
+ identities = cur.fetchall()
32
+ if len(identities) > 0:
33
+ logger.info(f"Found an existing PostgreSQL role for identity {app_identity_name}")
34
+ else:
35
+ logger.info(f"Creating a PostgreSQL role for identity {app_identity_name}")
36
+ cur.execute(f"SELECT * FROM pgaadauth_create_principal('{app_identity_name}', false, false)")
37
+
38
+ logger.info(f"Granting permissions to {app_identity_name}")
39
+ # set role to azure_pg_admin
40
+ cur.execute(f'GRANT USAGE ON SCHEMA public TO "{app_identity_name}"')
41
+ cur.execute(f'GRANT CREATE ON SCHEMA public TO "{app_identity_name}"')
42
+ cur.execute(f'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{app_identity_name}"')
43
+ cur.execute(
44
+ f"ALTER DEFAULT PRIVILEGES IN SCHEMA public "
45
+ f'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO "{app_identity_name}"'
46
+ )
47
+
48
+ cur.close()
49
+
50
+
51
+ if __name__ == "__main__":
52
+
53
+ logging.basicConfig(level=logging.WARNING)
54
+ logger.setLevel(logging.INFO)
55
+ parser = argparse.ArgumentParser(description="Create database schema")
56
+ parser.add_argument("--host", type=str, help="Postgres host")
57
+ parser.add_argument("--username", type=str, help="Postgres username")
58
+ parser.add_argument("--app-identity-name", type=str, help="Azure App Service identity name")
59
+
60
+ args = parser.parse_args()
61
+ if not args.host.endswith(".database.azure.com"):
62
+ logger.info("This script is intended to be used with Azure Database for PostgreSQL, not local PostgreSQL.")
63
+ exit(1)
64
+
65
+ assign_role_for_webapp(args.host, args.username, args.app_identity_name)
66
+ logger.info("Role created successfully.")