Pamela Fox commited on
Commit
d46392f
·
unverified ·
2 Parent(s): eb6044e 5f87718

Merge pull request #9 from pamelafox/secret-keyvault

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ * text=auto eol=lf
2
+ *.{cmd,[cC][mM][dD]} text eol=crlf
3
+ *.{bat,[bB][aA][tT]} text eol=crlf
.pre-commit-config.yaml CHANGED
@@ -1,17 +1,15 @@
1
  repos:
2
  - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v2.3.0
4
  hooks:
5
  - id: check-yaml
6
  - id: end-of-file-fixer
7
  - id: trailing-whitespace
8
  - repo: https://github.com/psf/black
9
- rev: 22.3.0
10
  hooks:
11
  - id: black
12
- args: ['--config=./pyproject.toml']
13
  - repo: https://github.com/charliermarsh/ruff-pre-commit
14
- rev: v0.0.121
15
  hooks:
16
  - id: ruff
17
- exclude: '.venv/'
 
1
  repos:
2
  - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.4.0
4
  hooks:
5
  - id: check-yaml
6
  - id: end-of-file-fixer
7
  - id: trailing-whitespace
8
  - repo: https://github.com/psf/black
9
+ rev: 23.1.0
10
  hooks:
11
  - id: black
 
12
  - repo: https://github.com/charliermarsh/ruff-pre-commit
13
+ rev: v0.0.258
14
  hooks:
15
  - id: ruff
 
README.md CHANGED
@@ -2,26 +2,42 @@
2
 
3
  # Quizzes app
4
 
5
- An example Django app that serves quizzes and lets people know how they scored.
6
- Quizzes and their questions are stored in a PostGreSQL database.
7
- There is no user authentication or per-user data stored.
8
 
9
- ## Local development
 
 
 
 
 
 
 
 
10
 
11
- Install the requirements and Git hooks:
12
 
13
- This project has devcontainer support, so you can open it in Github Codespaces or local VS Code with the Dev Containers extension. If you're unable to open the devcontainer,
14
- then it's best to first [create a Python virtual environment](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments) and activate that.
15
 
16
- 1. Install the requirements:
 
 
17
 
18
  ```shell
19
  python3 -m pip install -r requirements-dev.txt
20
  ```
21
 
22
- 2. Create an `.env` file using `.env.sample` as a guide. Set the value of `DBNAME` to the name of an existing database in your local PostgreSQL instance. Set the values of `DBHOST`, `DBUSER`, and `DBPASS` as appropriate for your local PostgreSQL instance. If you're in the devcontainer, copy the values exactly from `.env.sample`.
 
 
 
 
 
 
 
23
 
24
- 3. Fill in a secret value for `SECRET_KEY`. You can use this command to generate an appropriate value.
 
 
25
 
26
  ```shell
27
  python -c 'import secrets; print(secrets.token_hex())'
@@ -39,11 +55,11 @@ then it's best to first [create a Python virtual environment](https://docs.pytho
39
  python3 manage.py runserver
40
  ```
41
 
42
- 5. Navigate to "/quizzes" (since no "/" route is defined) to verify server is working.
43
 
44
  ### Admin
45
 
46
- This app comes with the built-in Django admin.
47
 
48
  1. Create a superuser:
49
 
@@ -86,7 +102,7 @@ azd up
86
  python manage.py createsuperuser
87
  ```
88
 
89
- ## CI/CD pipeline
90
 
91
  This project includes a Github workflow for deploying the resources to Azure
92
  on every push to main. That workflow requires several Azure-related authentication secrets
@@ -96,21 +112,34 @@ to be stored as Github action secrets. To set that up, run:
96
  azd pipeline config
97
  ```
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  ### Costs
100
 
101
  Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage.
102
 
103
- You can try the Azure pricing calculator for the resources:
104
 
105
  - Azure App Service: Basic Tier with 1 CPU core, 1.75GB RAM. Pricing is hourly. [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/)
106
  - PostgreSQL Flexible Server: Burstable Tier with 1 CPU core, 32GB storage. Pricing is hourly. [Pricing](https://azure.microsoft.com/pricing/details/postgresql/flexible-server/)
107
- - Virtual Network: Pricing based on data transfer. [Pricing](https://azure.microsoft.com/en-us/pricing/details/virtual-network/)
108
- - Private DNS Zone: Pricing based on number of zones per region per month. [Pricing](https://azure.microsoft.com/en-in/pricing/details/dns/)
109
  - Log analytics: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/)
110
 
111
  ⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use,
112
  either by deleting the resource group in the Portal or running `azd down`.
113
 
 
114
  ## Getting help
115
 
116
  If you're working with this project and running into issues, please post in **Discussions**.
 
2
 
3
  # Quizzes app
4
 
5
+ An example Django app that serves quizzes and lets people know how they scored. Quizzes and their questions are stored in a PostgreSQL database. There is no user authentication or per-user data stored.
 
 
6
 
7
+ ![Screenshot of Quiz page with question](readme_screenshot.png)
8
+
9
+ The project is designed for deployment on Azure App Service with a PostgreSQL flexible server. See deployment instructions below.
10
+
11
+ ![Diagram of the Architecture: App Service, PostgreSQL server, Key Vault, Log analytics](readme_diagram.png)
12
+
13
+ The code is tested with `django.test`, linted with [ruff](https://github.com/charliermarsh/ruff), and formatted with [black](https://black.readthedocs.io/en/stable/). Code quality issues are all checked with both [pre-commit](https://pre-commit.com/) and Github actions.
14
+
15
+ ## Opening the project
16
 
17
+ This project has [Dev Container support](https://code.visualstudio.com/docs/devcontainers/containers), so it will be be setup automatically if you open it in Github Codespaces or in local VS Code with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
18
 
19
+ If you're not using one of those options for opening the project, then you'll need to:
 
20
 
21
+ 1. Create a [Python virtual environment](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments) and activate it.
22
+
23
+ 2. Install the requirements:
24
 
25
  ```shell
26
  python3 -m pip install -r requirements-dev.txt
27
  ```
28
 
29
+ 3. Install the pre-commit hooks:
30
+
31
+ ```shell
32
+ pre-commit install
33
+ ```
34
+
35
+ ## Local development
36
+
37
 
38
+ 1. Create an `.env` file using `.env.sample` as a guide. Set the value of `DBNAME` to the name of an existing database in your local PostgreSQL instance. Set the values of `DBHOST`, `DBUSER`, and `DBPASS` as appropriate for your local PostgreSQL instance. If you're in the devcontainer, copy the values exactly from `.env.sample`.
39
+
40
+ 2. Fill in a secret value for `SECRET_KEY`. You can use this command to generate an appropriate value.
41
 
42
  ```shell
43
  python -c 'import secrets; print(secrets.token_hex())'
 
55
  python3 manage.py runserver
56
  ```
57
 
58
+ 5. Navigate to the displayed URL to verify the website is working.
59
 
60
  ### Admin
61
 
62
+ This app comes with the built-in Django admin interface.
63
 
64
  1. Create a superuser:
65
 
 
102
  python manage.py createsuperuser
103
  ```
104
 
105
+ ### CI/CD pipeline
106
 
107
  This project includes a Github workflow for deploying the resources to Azure
108
  on every push to main. That workflow requires several Azure-related authentication secrets
 
112
  azd pipeline config
113
  ```
114
 
115
+ ## Security
116
+
117
+ It is important to secure the databases in web applications to prevent unwanted data access.
118
+ This infrastructure uses the following mechanisms to secure the PostgreSQL database:
119
+
120
+ * Azure Firewall: The database is accessible only from other Azure IPs, not from public IPs. (Note that includes other customers using Azure).
121
+ * Admin Username: Unique string generated based on subscription ID and stored in Key Vault.
122
+ * Admin Password: Randomly generated and stored in Key Vault.
123
+ * PostgreSQL Version: Latest available on Azure, version 14, which includes security improvements.
124
+
125
+ ⚠️ For even more security, consider using an Azure Virtual Network to connect the Web App to the Database.
126
+ See [the Django-on-Azure project](https://github.com/tonybaloney/django-on-azure) for example infrastructure files.
127
+
128
  ### Costs
129
 
130
  Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage.
131
 
132
+ You can try the [Azure pricing calculator](https://azure.com/e/560b5f259111424daa7eb23c6848d164) for the resources:
133
 
134
  - Azure App Service: Basic Tier with 1 CPU core, 1.75GB RAM. Pricing is hourly. [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/)
135
  - PostgreSQL Flexible Server: Burstable Tier with 1 CPU core, 32GB storage. Pricing is hourly. [Pricing](https://azure.microsoft.com/pricing/details/postgresql/flexible-server/)
136
+ - Key Vault: Standard tier. Costs are per transaction, a few transactions are used on each deploy. [Pricing](https://azure.microsoft.com/pricing/details/key-vault/)
 
137
  - Log analytics: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/)
138
 
139
  ⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use,
140
  either by deleting the resource group in the Portal or running `azd down`.
141
 
142
+
143
  ## Getting help
144
 
145
  If you're working with this project and running into issues, please post in **Discussions**.
infra/core/database/postgresql/flexibleserver.bicep CHANGED
@@ -4,20 +4,19 @@ param tags object = {}
4
 
5
  param sku object
6
  param storage object
7
- param delegatedSubnetResourceId string = ''
8
- param privateDnsZoneArmResourceId string = ''
9
- param privateDnsZoneLink object = {}
10
-
11
- param databaseName string
12
  param administratorLogin string
13
  @secure()
14
  param administratorLoginPassword string
 
 
 
 
15
 
16
  // PostgreSQL version
17
- @allowed(['11', '12', '13', '14', '15'])
18
  param version string
19
 
20
- resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-01-20-preview' = {
 
21
  location: location
22
  tags: tags
23
  name: name
@@ -27,25 +26,39 @@ resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-01-20-pr
27
  administratorLogin: administratorLogin
28
  administratorLoginPassword: administratorLoginPassword
29
  storage: storage
30
- network: union(
31
- !empty(delegatedSubnetResourceId) ? { delegatedSubnetResourceId: delegatedSubnetResourceId } : {},
32
- !empty(privateDnsZoneArmResourceId) ? {privateDnsZoneArmResourceId: privateDnsZoneArmResourceId } : {})
33
  highAvailability: {
34
  mode: 'Disabled'
35
  }
36
  }
37
 
38
- resource database 'databases' = {
39
- name: databaseName
 
 
 
 
 
 
 
 
40
  }
41
 
42
- resource firewall 'firewallRules' = {
43
- name: 'AllowAllWindowsAzureIps'
44
  properties: {
45
  startIpAddress: '0.0.0.0'
46
  endIpAddress: '0.0.0.0'
47
  }
48
  }
49
 
50
- dependsOn: empty(privateDnsZoneLink) ? [] : [privateDnsZoneLink]
 
 
 
 
 
 
 
51
  }
 
 
 
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
13
+ param allowedSingleIPs array = []
14
 
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
 
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
infra/core/host/appservice.bicep CHANGED
@@ -33,10 +33,7 @@ param numberOfWorkers int = -1
33
  param scmDoBuildDuringDeployment bool = false
34
  param use32BitWorkerProcess bool = false
35
  param ftpsState string = 'FtpsOnly'
36
-
37
- // Microsoft.Web/sites/networkConfig
38
- param subnetResourceId string = ''
39
- param virtualNetwork object
40
 
41
  resource appService 'Microsoft.Web/sites@2022-03-01' = {
42
  name: name
@@ -49,11 +46,13 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = {
49
  linuxFxVersion: linuxFxVersion
50
  alwaysOn: alwaysOn
51
  ftpsState: ftpsState
 
52
  appCommandLine: appCommandLine
53
  numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
54
  minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
55
  use32BitWorkerProcess: use32BitWorkerProcess
56
  functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
 
57
  cors: {
58
  allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
59
  }
@@ -87,15 +86,6 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = {
87
  configAppSettings
88
  ]
89
  }
90
-
91
- resource webappVnetConfig 'networkConfig' = if (!(empty(virtualNetwork))) {
92
- name: 'virtualNetwork'
93
- properties: {
94
- subnetResourceId: subnetResourceId
95
- }
96
- }
97
-
98
- dependsOn: empty(virtualNetwork) ? [] : [virtualNetwork]
99
  }
100
 
101
  resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) {
 
33
  param scmDoBuildDuringDeployment bool = false
34
  param use32BitWorkerProcess bool = false
35
  param ftpsState string = 'FtpsOnly'
36
+ param healthCheckPath string = ''
 
 
 
37
 
38
  resource appService 'Microsoft.Web/sites@2022-03-01' = {
39
  name: name
 
46
  linuxFxVersion: linuxFxVersion
47
  alwaysOn: alwaysOn
48
  ftpsState: ftpsState
49
+ minTlsVersion: '1.2'
50
  appCommandLine: appCommandLine
51
  numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
52
  minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
53
  use32BitWorkerProcess: use32BitWorkerProcess
54
  functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
55
+ healthCheckPath: healthCheckPath
56
  cors: {
57
  allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
58
  }
 
86
  configAppSettings
87
  ]
88
  }
 
 
 
 
 
 
 
 
 
89
  }
90
 
91
  resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) {
infra/main.bicep CHANGED
@@ -9,9 +9,20 @@ 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 databasePassword string
 
 
 
 
 
 
 
15
 
16
  var resourceToken = toLower(uniqueString(subscription().id, name, location))
17
  var tags = { 'azd-env-name': name }
@@ -26,19 +37,7 @@ var prefix = '${name}-${resourceToken}'
26
 
27
  var postgresServerName = '${prefix}-postgresql'
28
 
29
- module virtualNetwork 'core/security/virtualnetwork.bicep' = {
30
- name: 'virtualnetwork'
31
- scope: resourceGroup
32
- params: {
33
- name: '${prefix}-vnet'
34
- location: location
35
- tags: tags
36
- postgresServerName: postgresServerName
37
- }
38
- }
39
-
40
- var databaseName = 'django'
41
- var databaseUser = 'django'
42
 
43
  module postgresServer 'core/database/postgresql/flexibleserver.bicep' = {
44
  name: 'postgresql'
@@ -54,13 +53,11 @@ module postgresServer 'core/database/postgresql/flexibleserver.bicep' = {
54
  storage: {
55
  storageSizeGB: 32
56
  }
57
- version: '13'
58
- administratorLogin: databaseUser
59
- administratorLoginPassword: databasePassword
60
- databaseName: databaseName
61
- delegatedSubnetResourceId: virtualNetwork.outputs.databaseSubnetId
62
- privateDnsZoneArmResourceId: virtualNetwork.outputs.privateDnsZoneId
63
- privateDnsZoneLink: virtualNetwork.outputs.privateDnsZoneLink
64
  }
65
  }
66
 
@@ -78,13 +75,13 @@ module web 'core/host/appservice.bicep' = {
78
  ftpsState: 'Disabled'
79
  managedIdentity: true
80
  appCommandLine: 'python manage.py migrate && gunicorn --workers 2 --threads 4 --timeout 60 --access-logfile \'-\' --error-logfile \'-\' --bind=0.0.0.0:8000 --chdir=/home/site/wwwroot quizsite.wsgi'
81
- virtualNetwork: virtualNetwork
82
- subnetResourceId: virtualNetwork.outputs.webSubnetId
83
  appSettings: {
 
84
  DBHOST: postgresServerName
85
- DBNAME: databaseName
86
- DBUSER: databaseUser
87
- DBPASS: databasePassword
 
88
  }
89
  }
90
  }
@@ -104,6 +101,54 @@ module appServicePlan 'core/host/appserviceplan.bicep' = {
104
  }
105
  }
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  module logAnalyticsWorkspace 'core/monitor/loganalytics.bicep' = {
108
  name: 'loganalytics'
109
  scope: resourceGroup
 
9
  @description('Primary location for all resources')
10
  param location string
11
 
12
+ @secure()
13
+ @description('PostGreSQL Server administrator username')
14
+ param postgresAdminUser string = 'admin${uniqueString(subscription().subscriptionId)}'
15
+
16
  @secure()
17
  @description('PostGreSQL Server administrator password')
18
+ param postgresAdminPassword string
19
+
20
+ @description('Id of the user or app to assign application roles')
21
+ param principalId string = ''
22
+
23
+ @secure()
24
+ @description('Django SECRET_KEY for cryptographic signing')
25
+ param djangoSecretKey string
26
 
27
  var resourceToken = toLower(uniqueString(subscription().id, name, location))
28
  var tags = { 'azd-env-name': name }
 
37
 
38
  var postgresServerName = '${prefix}-postgresql'
39
 
40
+ var postgresDatabaseName = 'django'
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  module postgresServer 'core/database/postgresql/flexibleserver.bicep' = {
43
  name: 'postgresql'
 
53
  storage: {
54
  storageSizeGB: 32
55
  }
56
+ version: '14'
57
+ administratorLogin: postgresAdminUser
58
+ administratorLoginPassword: postgresAdminPassword
59
+ databaseNames: [postgresDatabaseName]
60
+ allowAzureIPsFirewall: true
 
 
61
  }
62
  }
63
 
 
75
  ftpsState: 'Disabled'
76
  managedIdentity: true
77
  appCommandLine: 'python manage.py migrate && gunicorn --workers 2 --threads 4 --timeout 60 --access-logfile \'-\' --error-logfile \'-\' --bind=0.0.0.0:8000 --chdir=/home/site/wwwroot quizsite.wsgi'
 
 
78
  appSettings: {
79
+ ADMIN_URL: 'admin${uniqueString(appServicePlan.outputs.id)}'
80
  DBHOST: postgresServerName
81
+ DBNAME: postgresDatabaseName
82
+ DBUSER: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminUser)'
83
+ DBPASS: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminPassword)'
84
+ SECRET_KEY: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=djangoSecretKey)'
85
  }
86
  }
87
  }
 
101
  }
102
  }
103
 
104
+ module webKeyVaultAccess 'core/security/keyvault-access.bicep' = {
105
+ name: 'web-keyvault-access'
106
+ scope: resourceGroup
107
+ params: {
108
+ keyVaultName: keyVault.outputs.name
109
+ principalId: web.outputs.identityPrincipalId
110
+ }
111
+ }
112
+
113
+ // Store secrets in a keyvault
114
+ module keyVault './core/security/keyvault.bicep' = {
115
+ name: 'keyvault'
116
+ scope: resourceGroup
117
+ params: {
118
+ name: '${take(replace(prefix, '-', ''), 17)}-vault'
119
+ location: location
120
+ tags: tags
121
+ principalId: principalId
122
+ }
123
+ }
124
+
125
+ var secrets = [
126
+ {
127
+ name: 'djangoSecretKey'
128
+ value: djangoSecretKey
129
+ }
130
+ {
131
+ name: 'postgresAdminUser'
132
+ value: postgresAdminUser
133
+ }
134
+ {
135
+ name: 'postgresAdminPassword'
136
+ value: postgresAdminPassword
137
+ }
138
+ ]
139
+
140
+ @batchSize(1)
141
+ module keyVaultSecrets './core/security/keyvault-secret.bicep' = [for secret in secrets: {
142
+ name: 'keyvault-secret-${secret.name}'
143
+ scope: resourceGroup
144
+ params: {
145
+ keyVaultName: keyVault.outputs.name
146
+ name: secret.name
147
+ secretValue: secret.value
148
+ }
149
+ }]
150
+
151
+
152
  module logAnalyticsWorkspace 'core/monitor/loganalytics.bicep' = {
153
  name: 'loganalytics'
154
  scope: resourceGroup
infra/main.parameters.json CHANGED
@@ -8,8 +8,14 @@
8
  "location": {
9
  "value": "${AZURE_LOCATION}"
10
  },
11
- "databasePassword": {
12
- "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} databasePassword)"
 
 
 
 
 
 
13
  }
14
  }
15
  }
 
8
  "location": {
9
  "value": "${AZURE_LOCATION}"
10
  },
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
  }
pyproject.toml CHANGED
@@ -1,14 +1,14 @@
 
 
 
 
 
1
  [tool.black]
 
2
  line-length = 120
3
- target-version = ['py39']
4
  exclude = '''
5
  /(
6
  | \.venv
7
  | migrations
8
  )/
9
-
10
  '''
11
-
12
- [tool.ruff]
13
- line-length = 120
14
- ignore = ['D203']
 
1
+ [tool.ruff]
2
+ select = ["E", "F", "I", "UP"]
3
+ target-version = "py310"
4
+ line-length = 120
5
+
6
  [tool.black]
7
+ target-version = ['py310']
8
  line-length = 120
 
9
  exclude = '''
10
  /(
11
  | \.venv
12
  | migrations
13
  )/
 
14
  '''
 
 
 
 
quizsite/production.py CHANGED
@@ -6,6 +6,7 @@ import os
6
  ALLOWED_HOSTS = [os.environ["WEBSITE_HOSTNAME"]] if "WEBSITE_HOSTNAME" in os.environ else []
7
  CSRF_TRUSTED_ORIGINS = ["https://" + os.environ["WEBSITE_HOSTNAME"]] if "WEBSITE_HOSTNAME" in os.environ else []
8
  DEBUG = False
 
9
 
10
  # DBHOST is only the server name, not the full URL
11
  hostname = os.environ["DBHOST"]
@@ -19,5 +20,6 @@ DATABASES = {
19
  "HOST": hostname + ".postgres.database.azure.com",
20
  "USER": os.environ["DBUSER"],
21
  "PASSWORD": os.environ["DBPASS"],
 
22
  }
23
  }
 
6
  ALLOWED_HOSTS = [os.environ["WEBSITE_HOSTNAME"]] if "WEBSITE_HOSTNAME" in os.environ else []
7
  CSRF_TRUSTED_ORIGINS = ["https://" + os.environ["WEBSITE_HOSTNAME"]] if "WEBSITE_HOSTNAME" in os.environ else []
8
  DEBUG = False
9
+ ADMIN_URL = os.environ["ADMIN_URL"]
10
 
11
  # DBHOST is only the server name, not the full URL
12
  hostname = os.environ["DBHOST"]
 
20
  "HOST": hostname + ".postgres.database.azure.com",
21
  "USER": os.environ["DBUSER"],
22
  "PASSWORD": os.environ["DBPASS"],
23
+ "OPTIONS": {"sslmode": "require"},
24
  }
25
  }
quizsite/settings.py CHANGED
@@ -10,9 +10,8 @@ For the full list of settings and their values, see
10
  https://docs.djangoproject.com/en/4.1/ref/settings/
11
  """
12
 
13
- from pathlib import Path
14
  import os
15
-
16
 
17
  # Build paths inside the project like this: BASE_DIR / 'subdir'.
18
  BASE_DIR = Path(__file__).resolve().parent.parent
@@ -27,6 +26,8 @@ SECRET_KEY = os.getenv("SECRET_KEY")
27
  # SECURITY WARNING: don't run with debug turned on in production!
28
  DEBUG = True
29
 
 
 
30
  CSRF_TRUSTED_ORIGINS = [
31
  "http://localhost:8000",
32
  f"https://{os.getenv('CODESPACE_NAME')}-8000.{os.getenv('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}",
 
10
  https://docs.djangoproject.com/en/4.1/ref/settings/
11
  """
12
 
 
13
  import os
14
+ from pathlib import Path
15
 
16
  # Build paths inside the project like this: BASE_DIR / 'subdir'.
17
  BASE_DIR = Path(__file__).resolve().parent.parent
 
26
  # SECURITY WARNING: don't run with debug turned on in production!
27
  DEBUG = True
28
 
29
+ ADMIN_URL = "admin/"
30
+
31
  CSRF_TRUSTED_ORIGINS = [
32
  "http://localhost:8000",
33
  f"https://{os.getenv('CODESPACE_NAME')}-8000.{os.getenv('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}",
quizsite/urls.py CHANGED
@@ -13,10 +13,11 @@ Including another URLconf
13
  1. Import the include() function: from django.urls import include, path
14
  2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15
  """
 
16
  from django.contrib import admin
17
  from django.urls import include, path
18
 
19
  urlpatterns = [
20
- path("quizzes/", include("quizzes.urls")),
21
- path("admin/", admin.site.urls),
22
  ]
 
13
  1. Import the include() function: from django.urls import include, path
14
  2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15
  """
16
+ from django.conf import settings
17
  from django.contrib import admin
18
  from django.urls import include, path
19
 
20
  urlpatterns = [
21
+ path("", include("quizzes.urls")),
22
+ path(settings.ADMIN_URL, admin.site.urls),
23
  ]
quizzes/admin.py CHANGED
@@ -1,5 +1,6 @@
1
  from django.contrib import admin
2
- from .models import Quiz, Question, FreeTextAnswer, MultipleChoiceAnswer
 
3
 
4
  admin.site.register(Quiz)
5
 
 
1
  from django.contrib import admin
2
+
3
+ from .models import FreeTextAnswer, MultipleChoiceAnswer, Question, Quiz
4
 
5
  admin.site.register(Quiz)
6
 
quizzes/migrations/0001_initial.py CHANGED
@@ -1,12 +1,11 @@
1
  # Generated by Django 4.1.1 on 2022-09-14 18:17
2
 
3
  import django.contrib.postgres.fields
4
- from django.db import migrations, models
5
  import django.db.models.deletion
 
6
 
7
 
8
  class Migration(migrations.Migration):
9
-
10
  initial = True
11
 
12
  dependencies = []
@@ -46,9 +45,7 @@ class Migration(migrations.Migration):
46
  ),
47
  (
48
  "quiz",
49
- models.ForeignKey(
50
- on_delete=django.db.models.deletion.CASCADE, to="quizzes.quiz"
51
- ),
52
  ),
53
  ],
54
  ),
 
1
  # Generated by Django 4.1.1 on 2022-09-14 18:17
2
 
3
  import django.contrib.postgres.fields
 
4
  import django.db.models.deletion
5
+ from django.db import migrations, models
6
 
7
 
8
  class Migration(migrations.Migration):
 
9
  initial = True
10
 
11
  dependencies = []
 
45
  ),
46
  (
47
  "quiz",
48
+ models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="quizzes.quiz"),
 
 
49
  ),
50
  ],
51
  ),
quizzes/migrations/0002_remove_question_answer_status_and_more.py CHANGED
@@ -1,11 +1,10 @@
1
  # Generated by Django 4.1.1 on 2022-09-15 00:57
2
 
3
- from django.db import migrations, models
4
  import django.db.models.deletion
 
5
 
6
 
7
  class Migration(migrations.Migration):
8
-
9
  dependencies = [
10
  ("quizzes", "0001_initial"),
11
  ]
@@ -18,15 +17,11 @@ class Migration(migrations.Migration):
18
  migrations.AlterField(
19
  model_name="freetextanswer",
20
  name="question",
21
- field=models.OneToOneField(
22
- on_delete=django.db.models.deletion.CASCADE, to="quizzes.question"
23
- ),
24
  ),
25
  migrations.AlterField(
26
  model_name="multiplechoiceanswer",
27
  name="question",
28
- field=models.OneToOneField(
29
- on_delete=django.db.models.deletion.CASCADE, to="quizzes.question"
30
- ),
31
  ),
32
  ]
 
1
  # Generated by Django 4.1.1 on 2022-09-15 00:57
2
 
 
3
  import django.db.models.deletion
4
+ from django.db import migrations, models
5
 
6
 
7
  class Migration(migrations.Migration):
 
8
  dependencies = [
9
  ("quizzes", "0001_initial"),
10
  ]
 
17
  migrations.AlterField(
18
  model_name="freetextanswer",
19
  name="question",
20
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="quizzes.question"),
 
 
21
  ),
22
  migrations.AlterField(
23
  model_name="multiplechoiceanswer",
24
  name="question",
25
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="quizzes.question"),
 
 
26
  ),
27
  ]
quizzes/models.py CHANGED
@@ -1,5 +1,5 @@
1
- from django.db import models
2
  from django.contrib.postgres import fields
 
3
 
4
 
5
  class Quiz(models.Model):
 
 
1
  from django.contrib.postgres import fields
2
+ from django.db import models
3
 
4
 
5
  class Quiz(models.Model):
quizzes/tests.py CHANGED
@@ -1,7 +1,7 @@
1
  from django.test import TestCase
2
  from django.urls import reverse
3
 
4
- from .models import Quiz, Question, FreeTextAnswer, MultipleChoiceAnswer
5
 
6
 
7
  def create_quiz():
 
1
  from django.test import TestCase
2
  from django.urls import reverse
3
 
4
+ from .models import FreeTextAnswer, MultipleChoiceAnswer, Question, Quiz
5
 
6
 
7
  def create_quiz():
quizzes/urls.py CHANGED
@@ -6,7 +6,7 @@ app_name = "quizzes"
6
 
7
  urlpatterns = [
8
  path("", views.IndexView.as_view(), name="index"),
9
- path("<int:quiz_id>/", views.display_quiz, name="display_quiz"),
10
- path("<int:quiz_id>/questions/<int:question_id>", views.display_question, name="display_question"),
11
  path("questions/<int:question_id>/grade/", views.grade_question, name="grade_question"),
12
  ]
 
6
 
7
  urlpatterns = [
8
  path("", views.IndexView.as_view(), name="index"),
9
+ path("quizzes/<int:quiz_id>/", views.display_quiz, name="display_quiz"),
10
+ path("quizzes/<int:quiz_id>/questions/<int:question_id>", views.display_question, name="display_question"),
11
  path("questions/<int:question_id>/grade/", views.grade_question, name="grade_question"),
12
  ]
quizzes/views.py CHANGED
@@ -1,8 +1,8 @@
1
- from django.shortcuts import get_object_or_404, render, redirect
2
  from django.urls import reverse
3
  from django.views import generic
4
 
5
- from .models import Quiz, Question
6
 
7
 
8
  class IndexView(generic.ListView):
 
1
+ from django.shortcuts import get_object_or_404, redirect, render
2
  from django.urls import reverse
3
  from django.views import generic
4
 
5
+ from .models import Question, Quiz
6
 
7
 
8
  class IndexView(generic.ListView):
readme_diagram.png ADDED
readme_screenshot.png ADDED