dbca-utils 2.1.3__tar.gz → 2.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbca-utils
3
- Version: 2.1.3
3
+ Version: 2.1.5
4
4
  Summary: Utilities for DBCA Django apps
5
5
  Author-Email: Rocky Chen <rocky.chen@dbca.wa.gov.au>, Ashley Felton <ashley.felton@dbca.wa.gov.au>
6
6
  License-Expression: Apache-2.0
7
7
  Classifier: Framework :: Django
8
- Classifier: Framework :: Django :: 4.0
9
8
  Classifier: Framework :: Django :: 4.2
10
9
  Classifier: Framework :: Django :: 5.0
11
10
  Classifier: Framework :: Django :: 5.2
@@ -25,26 +24,24 @@ Project-URL: Repository, https://github.com/dbca-wa/dbca-utils.git
25
24
  Project-URL: Changelog, https://github.com/dbca-wa/dbca-utils/blob/master/CHANGELOG.md
26
25
  Project-URL: GitHub, https://github.com/dbca-wa/dbca-utils
27
26
  Requires-Python: <4.0,>=3.10
28
- Requires-Dist: django<6,>=4
29
- Requires-Dist: markupsafe==3.0.2
27
+ Requires-Dist: django<6,>=4.2
28
+ Requires-Dist: markupsafe==3.0.3
30
29
  Description-Content-Type: text/markdown
31
30
 
32
31
  # Overview
33
32
 
34
33
  DBCA Django utility classes and functions.
35
34
 
36
- ## Development
35
+ ## Requirements
37
36
 
38
- The recommended way to set up this project for development is using
39
- [uv](https://docs.astral.sh/uv/)
40
- to install and manage a Python virtual environment.
41
- With uv installed, install the required Python version (see `pyproject.toml`). Example:
37
+ - Python 3.10 or later
38
+ - Django 4.2 or later
42
39
 
43
- uv python install 3.12
40
+ ## Development
44
41
 
45
- Change into the project directory and run:
42
+ Dependencies for this project are managed using [uv](https://docs.astral.sh/uv/).
43
+ With uv installed, change into the project directory and run:
46
44
 
47
- uv python pin 3.12
48
45
  uv sync
49
46
 
50
47
  Activate the virtualenv like so:
@@ -61,17 +58,15 @@ Run unit tests using `pytest` (or `tox`, to test against multiple Python version
61
58
  Tagged releases are built and pushed to PyPI automatically using a GitHub
62
59
  workflow in the project. Update the project version in `pyproject.toml` and
63
60
  tag the required commit with the same value to trigger a release. Packages
64
- can also be built and uploaded manually, if desired.
65
-
66
- Build the project locally using uv, [publish to the PyPI registry](https://docs.astral.sh/uv/guides/publish/#publishing-your-package)
67
- using the same tool if you require:
61
+ can also be built and uploaded manually to PyPI using [uv](https://docs.astral.sh/uv/guides/publish/#publishing-your-package),
62
+ if required:
68
63
 
69
64
  uv build
70
65
  uv publish
71
66
 
72
67
  ## Installation
73
68
 
74
- 1. Install via pip/etc.: `pip install dbca-utils`
69
+ 1. Install via uv/pip/etc.: `pip install dbca-utils`
75
70
 
76
71
  ## SSO Login Middleware
77
72
 
@@ -2,18 +2,16 @@
2
2
 
3
3
  DBCA Django utility classes and functions.
4
4
 
5
- ## Development
5
+ ## Requirements
6
6
 
7
- The recommended way to set up this project for development is using
8
- [uv](https://docs.astral.sh/uv/)
9
- to install and manage a Python virtual environment.
10
- With uv installed, install the required Python version (see `pyproject.toml`). Example:
7
+ - Python 3.10 or later
8
+ - Django 4.2 or later
11
9
 
12
- uv python install 3.12
10
+ ## Development
13
11
 
14
- Change into the project directory and run:
12
+ Dependencies for this project are managed using [uv](https://docs.astral.sh/uv/).
13
+ With uv installed, change into the project directory and run:
15
14
 
16
- uv python pin 3.12
17
15
  uv sync
18
16
 
19
17
  Activate the virtualenv like so:
@@ -30,17 +28,15 @@ Run unit tests using `pytest` (or `tox`, to test against multiple Python version
30
28
  Tagged releases are built and pushed to PyPI automatically using a GitHub
31
29
  workflow in the project. Update the project version in `pyproject.toml` and
32
30
  tag the required commit with the same value to trigger a release. Packages
33
- can also be built and uploaded manually, if desired.
34
-
35
- Build the project locally using uv, [publish to the PyPI registry](https://docs.astral.sh/uv/guides/publish/#publishing-your-package)
36
- using the same tool if you require:
31
+ can also be built and uploaded manually to PyPI using [uv](https://docs.astral.sh/uv/guides/publish/#publishing-your-package),
32
+ if required:
37
33
 
38
34
  uv build
39
35
  uv publish
40
36
 
41
37
  ## Installation
42
38
 
43
- 1. Install via pip/etc.: `pip install dbca-utils`
39
+ 1. Install via uv/pip/etc.: `pip install dbca-utils`
44
40
 
45
41
  ## SSO Login Middleware
46
42
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbca-utils"
3
- version = "2.1.3"
3
+ version = "2.1.5"
4
4
  description = "Utilities for DBCA Django apps"
5
5
  authors = [
6
6
  { name = "Rocky Chen", email = "rocky.chen@dbca.wa.gov.au" },
@@ -10,7 +10,6 @@ readme = "README.md"
10
10
  license = "Apache-2.0"
11
11
  classifiers = [
12
12
  "Framework :: Django",
13
- "Framework :: Django :: 4.0",
14
13
  "Framework :: Django :: 4.2",
15
14
  "Framework :: Django :: 5.0",
16
15
  "Framework :: Django :: 5.2",
@@ -28,8 +27,8 @@ classifiers = [
28
27
  ]
29
28
  requires-python = ">=3.10,<4.0"
30
29
  dependencies = [
31
- "django>=4,<6",
32
- "markupsafe==3.0.2",
30
+ "django>=4.2,<6",
31
+ "markupsafe==3.0.3",
33
32
  ]
34
33
 
35
34
  [project.urls]
@@ -40,10 +39,10 @@ GitHub = "https://github.com/dbca-wa/dbca-utils"
40
39
 
41
40
  [dependency-groups]
42
41
  dev = [
43
- "pytest-django>=4.11.0",
44
- "pytest-sugar>=1.0.0",
45
- "tox>=4.25.0",
46
- "tox-uv>=1.25.0",
42
+ "pytest-django>=4.11.1",
43
+ "pytest-sugar>=1.1.1",
44
+ "tox>=4.31.0",
45
+ "tox-uv>=1.29.0",
47
46
  ]
48
47
 
49
48
  [build-system]
@@ -5,6 +5,7 @@ from django.contrib.auth.middleware import AuthenticationMiddleware, get_user
5
5
  from django.utils import timezone
6
6
  from django.utils.deprecation import MiddlewareMixin
7
7
  from django.utils.functional import SimpleLazyObject
8
+ from django.utils.html import strip_tags
8
9
  from markupsafe import escape
9
10
 
10
11
  from dbca_utils.utils import env
@@ -86,9 +87,9 @@ class SSOLoginMiddleware(MiddlewareMixin):
86
87
  """Django middleware to process HTTP requests containing headers set by the Auth2
87
88
  SSO service, specificially:
88
89
  - `HTTP_REMOTE_USER`
90
+ - `HTTP_X_EMAIL`
89
91
  - `HTTP_X_LAST_NAME`
90
92
  - `HTTP_X_FIRST_NAME`
91
- - `HTTP_X_EMAIL`
92
93
  The middleware assesses requests containing these headers, and (having deferred user
93
94
  authentication to the upstream service), retrieves the local Django User and logs
94
95
  the user in automatically.
@@ -123,21 +124,24 @@ class SSOLoginMiddleware(MiddlewareMixin):
123
124
  if not request.user.is_authenticated:
124
125
  attributemap = {
125
126
  "username": "HTTP_REMOTE_USER",
127
+ "email": "HTTP_X_EMAIL",
126
128
  "last_name": "HTTP_X_LAST_NAME",
127
129
  "first_name": "HTTP_X_FIRST_NAME",
128
- "email": "HTTP_X_EMAIL",
129
130
  }
131
+ attributes = {"email": "", "username": ""}
130
132
 
131
- for key, value in attributemap.items():
132
- if value in request.META:
133
- attributemap[key] = request.META[value]
133
+ for key, meta_value in attributemap.items():
134
+ if meta_value in request.META:
135
+ attributes[key] = request.META[meta_value]
134
136
 
135
137
  # Sanitise first_name and last_name values, because end-users have control over these
136
138
  # values and could conceivably inject malicious values into them (e.g. a XSS attack).
137
- if "first_name" in attributemap:
138
- attributemap["first_name"] = str(escape(attributemap["first_name"]))
139
- if "last_name" in attributemap:
140
- attributemap["last_name"] = str(escape(attributemap["last_name"]))
139
+ if "first_name" in attributes:
140
+ attributes["first_name"] = strip_tags(attributes["first_name"])
141
+ attributes["first_name"] = str(escape(attributes["first_name"]))
142
+ if "last_name" in attributes:
143
+ attributes["last_name"] = strip_tags(attributes["last_name"])
144
+ attributes["last_name"] = str(escape(attributes["last_name"]))
141
145
 
142
146
  # Optional setting: projects may define accepted user email domains either as
143
147
  # a list of strings, or a single string.
@@ -147,20 +151,26 @@ class SSOLoginMiddleware(MiddlewareMixin):
147
151
  else:
148
152
  allowed_email_suffixes = settings.ALLOWED_EMAIL_SUFFIXES
149
153
  # If the user email suffix is not in the allowed list, return a 404 response.
150
- if not any([attributemap["email"].lower().endswith(suffix) for suffix in allowed_email_suffixes]):
154
+ if not any([attributes["email"].lower().endswith(suffix) for suffix in allowed_email_suffixes]):
151
155
  return http.HttpResponseForbidden()
152
156
 
153
157
  # Check for an existing User instance.
154
- if attributemap["email"] and User.objects.filter(email__iexact=attributemap["email"]).exists():
155
- user = User.objects.filter(email__iexact=attributemap["email"])[0]
156
- elif User.__name__ != "EmailUser" and User.objects.filter(username__iexact=attributemap["username"]).exists():
157
- user = User.objects.filter(username__iexact=attributemap["username"])[0]
158
+ if attributes["email"] and User.objects.filter(email__iexact=attributes["email"]).exists():
159
+ user = User.objects.filter(email__iexact=attributes["email"]).first()
160
+ elif User.__name__ != "EmailUser" and User.objects.filter(username__iexact=attributes["username"]).exists():
161
+ user = User.objects.filter(username__iexact=attributes["username"]).first()
158
162
  else:
159
163
  user = User(last_login=timezone.localtime())
160
164
 
161
165
  # Set the user's details from the supplied information.
162
- user.__dict__.update(attributemap)
163
- user.save()
166
+ user_has_changed = False
167
+ for attr, value in attributes.items():
168
+ if getattr(user, attr) != value:
169
+ setattr(user, attr, value)
170
+ user_has_changed = True
171
+ if user_has_changed:
172
+ user.save()
173
+
164
174
  user.backend = "django.contrib.auth.backends.ModelBackend"
165
175
 
166
176
  # Log the user in.
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>{{ TITLE }}</title>
6
+ </head>
7
+ <body>
8
+ <div>Authenticated: {{ user.is_authenticated }}</div>
9
+ <div>Email: {{ user.email }}</div>
10
+ <div>Name: {{ user.get_full_name }}</div>
11
+ <ul>
12
+ {% for object in object_list %}
13
+ <li>{{ object.name }}, {{ object.created|date:"r" }}</li>
14
+ {% endfor %}
15
+ </ul>
16
+ </body>
17
+ </html>
@@ -26,7 +26,7 @@ os.environ["TEST_TUPLE"] = "('a', 'b', 'c')"
26
26
  os.environ["TEST_BOOL"] = "False"
27
27
 
28
28
 
29
- class TestUtils(TestCase):
29
+ class UtilsTest(TestCase):
30
30
  def test_env_returns_str(self):
31
31
  test_str = env("TEST_STR")
32
32
  self.assertTrue(isinstance(test_str, str))
@@ -68,14 +68,12 @@ class TestUtils(TestCase):
68
68
  self.assertTrue(isinstance(test_str, str))
69
69
 
70
70
 
71
- class TestModelTests(TestCase):
71
+ class ModelTest(TestCase):
72
72
  client = Client()
73
73
  model = TestModel
74
74
 
75
75
  def setUp(self):
76
- self.user = User.objects.create_user(
77
- username="test", email="test@email.com", password="secret"
78
- )
76
+ self.user = User.objects.create_user(username="test", email="test@email.com", password="secret")
79
77
  self.test_model = TestModel.objects.create(name=TEST_NAME)
80
78
  self.client.login(username="test", password="secret")
81
79
 
@@ -109,3 +107,53 @@ class TestModelTests(TestCase):
109
107
  self.assertEqual(response.status_code, 200)
110
108
  self.assertContains(response, TEST_VAR)
111
109
  self.assertContains(response, TEST_NAME)
110
+
111
+
112
+ class SSOLoginMiddlewareTest(TestCase):
113
+ client = Client()
114
+
115
+ def setUp(self):
116
+ self.user = User.objects.create_user(username="testuser", email="testuser@email.com")
117
+ # Ensure that the client starts logged out.
118
+ self.client.logout()
119
+
120
+ def test_sso_login(self):
121
+ """Test that requests having the required META headers will automatically sign in users."""
122
+ url = reverse("test_model_list")
123
+ response = self.client.get(
124
+ url,
125
+ HTTP_REMOTE_USER="testuser",
126
+ HTTP_X_EMAIL="testuser@email.com",
127
+ )
128
+ self.assertEqual(response.status_code, 200)
129
+ self.assertContains(response, "Authenticated: True")
130
+
131
+ def test_user_properties_update(self):
132
+ """Test that requests with relevant META headers will update user properties."""
133
+ url = reverse("test_model_list")
134
+
135
+ response = self.client.get(
136
+ url,
137
+ HTTP_REMOTE_USER="testuser",
138
+ HTTP_X_EMAIL="testuser@email.com",
139
+ )
140
+ self.assertEqual(response.status_code, 200)
141
+ self.assertContains(response, "Authenticated: True")
142
+ user = User.objects.get(email="testuser@email.com")
143
+ self.assertEqual(user.first_name, "")
144
+ self.assertEqual(user.last_name, "")
145
+
146
+ # Simulate a name change via SSO.
147
+ self.client.logout()
148
+ response = self.client.get(
149
+ url,
150
+ HTTP_REMOTE_USER="testuser",
151
+ HTTP_X_EMAIL="testuser@email.com",
152
+ HTTP_X_FIRST_NAME="Test",
153
+ HTTP_X_LAST_NAME="User",
154
+ )
155
+ self.assertEqual(response.status_code, 200)
156
+ self.assertContains(response, "Name: Test User")
157
+ user = User.objects.get(email="testuser@email.com")
158
+ self.assertEqual(user.first_name, "Test")
159
+ self.assertEqual(user.last_name, "User")
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>{{ TITLE }}</title>
6
- </head>
7
- <body>
8
- <ul>
9
- {% for object in object_list %}<li>{{ object.name }}, {{ object.created|date:"r" }}</li>{% endfor %}
10
- </ul>
11
- </body>
12
- </html>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes