dbca-utils 2.1.4__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbca-utils
3
- Version: 2.1.4
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
@@ -58,10 +58,8 @@ Run unit tests using `pytest` (or `tox`, to test against multiple Python version
58
58
  Tagged releases are built and pushed to PyPI automatically using a GitHub
59
59
  workflow in the project. Update the project version in `pyproject.toml` and
60
60
  tag the required commit with the same value to trigger a release. Packages
61
- can also be built and uploaded manually, if desired.
62
-
63
- Build the project locally using uv, [publish to the PyPI registry](https://docs.astral.sh/uv/guides/publish/#publishing-your-package)
64
- 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:
65
63
 
66
64
  uv build
67
65
  uv publish
@@ -28,10 +28,8 @@ Run unit tests using `pytest` (or `tox`, to test against multiple Python version
28
28
  Tagged releases are built and pushed to PyPI automatically using a GitHub
29
29
  workflow in the project. Update the project version in `pyproject.toml` and
30
30
  tag the required commit with the same value to trigger a release. Packages
31
- can also be built and uploaded manually, if desired.
32
-
33
- Build the project locally using uv, [publish to the PyPI registry](https://docs.astral.sh/uv/guides/publish/#publishing-your-package)
34
- 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:
35
33
 
36
34
  uv build
37
35
  uv publish
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbca-utils"
3
- version = "2.1.4"
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" },
@@ -87,9 +87,9 @@ class SSOLoginMiddleware(MiddlewareMixin):
87
87
  """Django middleware to process HTTP requests containing headers set by the Auth2
88
88
  SSO service, specificially:
89
89
  - `HTTP_REMOTE_USER`
90
+ - `HTTP_X_EMAIL`
90
91
  - `HTTP_X_LAST_NAME`
91
92
  - `HTTP_X_FIRST_NAME`
92
- - `HTTP_X_EMAIL`
93
93
  The middleware assesses requests containing these headers, and (having deferred user
94
94
  authentication to the upstream service), retrieves the local Django User and logs
95
95
  the user in automatically.
@@ -124,23 +124,24 @@ class SSOLoginMiddleware(MiddlewareMixin):
124
124
  if not request.user.is_authenticated:
125
125
  attributemap = {
126
126
  "username": "HTTP_REMOTE_USER",
127
+ "email": "HTTP_X_EMAIL",
127
128
  "last_name": "HTTP_X_LAST_NAME",
128
129
  "first_name": "HTTP_X_FIRST_NAME",
129
- "email": "HTTP_X_EMAIL",
130
130
  }
131
+ attributes = {"email": "", "username": ""}
131
132
 
132
- for key, value in attributemap.items():
133
- if value in request.META:
134
- 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]
135
136
 
136
137
  # Sanitise first_name and last_name values, because end-users have control over these
137
138
  # values and could conceivably inject malicious values into them (e.g. a XSS attack).
138
- if "first_name" in attributemap:
139
- attributemap["first_name"] = strip_tags(attributemap["first_name"])
140
- attributemap["first_name"] = str(escape(attributemap["first_name"]))
141
- if "last_name" in attributemap:
142
- attributemap["last_name"] = strip_tags(attributemap["first_name"])
143
- 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"]))
144
145
 
145
146
  # Optional setting: projects may define accepted user email domains either as
146
147
  # a list of strings, or a single string.
@@ -150,20 +151,26 @@ class SSOLoginMiddleware(MiddlewareMixin):
150
151
  else:
151
152
  allowed_email_suffixes = settings.ALLOWED_EMAIL_SUFFIXES
152
153
  # If the user email suffix is not in the allowed list, return a 404 response.
153
- 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]):
154
155
  return http.HttpResponseForbidden()
155
156
 
156
157
  # Check for an existing User instance.
157
- if attributemap["email"] and User.objects.filter(email__iexact=attributemap["email"]).exists():
158
- user = User.objects.filter(email__iexact=attributemap["email"])[0]
159
- elif User.__name__ != "EmailUser" and User.objects.filter(username__iexact=attributemap["username"]).exists():
160
- 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()
161
162
  else:
162
163
  user = User(last_login=timezone.localtime())
163
164
 
164
165
  # Set the user's details from the supplied information.
165
- user.__dict__.update(attributemap)
166
- 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
+
167
174
  user.backend = "django.contrib.auth.backends.ModelBackend"
168
175
 
169
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