Pamela Fox
commited on
Commit
·
f24be86
1
Parent(s):
cd77237
Port to passwordless
Browse files- azure.yaml +7 -0
- infra/core/database/postgresql/flexibleserver.bicep +93 -27
- infra/core/security/keyvault.bicep +1 -9
- infra/core/security/role.bicep +21 -0
- infra/main.bicep +53 -19
- infra/main.parameters.json +11 -2
- scripts/load_python_env.sh +7 -0
- scripts/requirements.txt +2 -0
- scripts/setup_postgres_azurerole.ps1 +18 -0
- scripts/setup_postgres_azurerole.sh +12 -0
- src/quizsite/postgresql/__init__.py +0 -0
- src/quizsite/postgresql/base.py +12 -0
- src/quizsite/settings.py +3 -2
- src/requirements.txt +1 -0
- src/setup_postgres_azurerole.py +66 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
44 |
}
|
|
|
45 |
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
52 |
}
|
|
|
53 |
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
}
|
63 |
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
@
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
54 |
-
|
|
|
|
|
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:
|
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:
|
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 |
-
"
|
15 |
-
"value": "$
|
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":
|
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.")
|