dbca-utils 2.1.4__tar.gz → 2.2.0__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.
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/PKG-INFO +11 -15
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/README.md +4 -6
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/pyproject.toml +10 -12
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/src/dbca_utils/middleware.py +31 -20
- dbca_utils-2.2.0/tests/templates/tests/test_model_list.html +17 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/tests.py +53 -5
- dbca_utils-2.1.4/tests/templates/tests/test_model_list.html +0 -12
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/LICENSE +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/src/dbca_utils/__init__.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/src/dbca_utils/models.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/src/dbca_utils/utils.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/__init__.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/apps.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/migrations/0001_initial.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/migrations/__init__.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/models.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/settings.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/urls.py +0 -0
- {dbca_utils-2.1.4 → dbca_utils-2.2.0}/tests/views.py +0 -0
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbca-utils
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
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.2
|
|
9
|
-
Classifier: Framework :: Django :: 5.0
|
|
10
8
|
Classifier: Framework :: Django :: 5.2
|
|
9
|
+
Classifier: Framework :: Django :: 6.0
|
|
11
10
|
Classifier: Environment :: Web Environment
|
|
12
11
|
Classifier: Intended Audience :: Developers
|
|
13
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
13
|
Classifier: Programming Language :: Python
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
18
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
20
|
Project-URL: Homepage, https://github.com/dbca-wa/dbca-utils
|
|
23
21
|
Project-URL: Repository, https://github.com/dbca-wa/dbca-utils.git
|
|
24
|
-
Project-URL: Changelog, https://github.com/dbca-wa/dbca-utils/blob/
|
|
22
|
+
Project-URL: Changelog, https://github.com/dbca-wa/dbca-utils/blob/main/CHANGELOG.md
|
|
25
23
|
Project-URL: GitHub, https://github.com/dbca-wa/dbca-utils
|
|
26
|
-
Requires-Python: <4.0,>=3.
|
|
27
|
-
Requires-Dist: django<6,>=
|
|
28
|
-
Requires-Dist: markupsafe
|
|
24
|
+
Requires-Python: <4.0,>=3.12
|
|
25
|
+
Requires-Dist: django<6.2,>=5.2
|
|
26
|
+
Requires-Dist: markupsafe>=3.0.3
|
|
29
27
|
Description-Content-Type: text/markdown
|
|
30
28
|
|
|
31
29
|
# Overview
|
|
@@ -34,8 +32,8 @@ DBCA Django utility classes and functions.
|
|
|
34
32
|
|
|
35
33
|
## Requirements
|
|
36
34
|
|
|
37
|
-
- Python 3.
|
|
38
|
-
- Django
|
|
35
|
+
- Python 3.12 or later
|
|
36
|
+
- Django 5.2 or later
|
|
39
37
|
|
|
40
38
|
## Development
|
|
41
39
|
|
|
@@ -58,10 +56,8 @@ Run unit tests using `pytest` (or `tox`, to test against multiple Python version
|
|
|
58
56
|
Tagged releases are built and pushed to PyPI automatically using a GitHub
|
|
59
57
|
workflow in the project. Update the project version in `pyproject.toml` and
|
|
60
58
|
tag the required commit with the same value to trigger a release. Packages
|
|
61
|
-
can also be built and uploaded manually
|
|
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:
|
|
59
|
+
can also be built and uploaded manually to PyPI using [uv](https://docs.astral.sh/uv/guides/publish/#publishing-your-package),
|
|
60
|
+
if required:
|
|
65
61
|
|
|
66
62
|
uv build
|
|
67
63
|
uv publish
|
|
@@ -4,8 +4,8 @@ DBCA Django utility classes and functions.
|
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
- Python 3.
|
|
8
|
-
- Django
|
|
7
|
+
- Python 3.12 or later
|
|
8
|
+
- Django 5.2 or later
|
|
9
9
|
|
|
10
10
|
## Development
|
|
11
11
|
|
|
@@ -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
|
|
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.
|
|
3
|
+
version = "2.2.0"
|
|
4
4
|
description = "Utilities for DBCA Django apps"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Rocky Chen", email = "rocky.chen@dbca.wa.gov.au" },
|
|
@@ -10,39 +10,37 @@ readme = "README.md"
|
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Framework :: Django",
|
|
13
|
-
"Framework :: Django :: 4.2",
|
|
14
|
-
"Framework :: Django :: 5.0",
|
|
15
13
|
"Framework :: Django :: 5.2",
|
|
14
|
+
"Framework :: Django :: 6.0",
|
|
16
15
|
"Environment :: Web Environment",
|
|
17
16
|
"Intended Audience :: Developers",
|
|
18
17
|
"Development Status :: 5 - Production/Stable",
|
|
19
18
|
"Programming Language :: Python",
|
|
20
19
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3.10",
|
|
22
|
-
"Programming Language :: Python :: 3.11",
|
|
23
20
|
"Programming Language :: Python :: 3.12",
|
|
24
21
|
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
25
23
|
"Topic :: Software Development :: Libraries",
|
|
26
24
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
25
|
]
|
|
28
|
-
requires-python = ">=3.
|
|
26
|
+
requires-python = ">=3.12,<4.0"
|
|
29
27
|
dependencies = [
|
|
30
|
-
"django>=
|
|
31
|
-
"markupsafe
|
|
28
|
+
"django>=5.2,<6.2",
|
|
29
|
+
"markupsafe>=3.0.3",
|
|
32
30
|
]
|
|
33
31
|
|
|
34
32
|
[project.urls]
|
|
35
33
|
Homepage = "https://github.com/dbca-wa/dbca-utils"
|
|
36
34
|
Repository = "https://github.com/dbca-wa/dbca-utils.git"
|
|
37
|
-
Changelog = "https://github.com/dbca-wa/dbca-utils/blob/
|
|
35
|
+
Changelog = "https://github.com/dbca-wa/dbca-utils/blob/main/CHANGELOG.md"
|
|
38
36
|
GitHub = "https://github.com/dbca-wa/dbca-utils"
|
|
39
37
|
|
|
40
38
|
[dependency-groups]
|
|
41
39
|
dev = [
|
|
42
|
-
"pytest-django>=4.
|
|
40
|
+
"pytest-django>=4.12.0",
|
|
43
41
|
"pytest-sugar>=1.1.1",
|
|
44
|
-
"tox>=4.
|
|
45
|
-
"tox-uv>=1.
|
|
42
|
+
"tox>=4.53.0",
|
|
43
|
+
"tox-uv>=1.35.0",
|
|
46
44
|
]
|
|
47
45
|
|
|
48
46
|
[build-system]
|
|
@@ -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.
|
|
@@ -116,7 +116,7 @@ class SSOLoginMiddleware(MiddlewareMixin):
|
|
|
116
116
|
# Security check: if the logged-in request user's email does not match the email
|
|
117
117
|
# returned from Auth2, invalidate the current request session and force a new session
|
|
118
118
|
# using the returned SSO values.
|
|
119
|
-
if request.user.is_authenticated and request.user.email != request.META
|
|
119
|
+
if request.user.is_authenticated and request.user.email != request.META.get("HTTP_X_EMAIL", ""):
|
|
120
120
|
logout(request)
|
|
121
121
|
|
|
122
122
|
# Request user is not authenticated locally: obtain user attributes from the request.META dict
|
|
@@ -124,46 +124,57 @@ 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 = {"username": ""}
|
|
131
132
|
|
|
132
|
-
for key,
|
|
133
|
-
if
|
|
134
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if "last_name" in
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
147
148
|
if hasattr(settings, "ALLOWED_EMAIL_SUFFIXES") and settings.ALLOWED_EMAIL_SUFFIXES:
|
|
148
149
|
if isinstance(settings.ALLOWED_EMAIL_SUFFIXES, str):
|
|
149
|
-
|
|
150
|
+
# If configured as a string, ALLOWED_EMAIL_SUFFIXES must be a comma-separated list (single-item list is OK).
|
|
151
|
+
allowed_email_suffixes = settings.ALLOWED_EMAIL_SUFFIXES.split(",")
|
|
150
152
|
else:
|
|
151
153
|
allowed_email_suffixes = settings.ALLOWED_EMAIL_SUFFIXES
|
|
154
|
+
# Validation: allowed_email_suffixes must be a list of strings.
|
|
155
|
+
if not (isinstance(allowed_email_suffixes, list) and all(isinstance(x, str) for x in allowed_email_suffixes)):
|
|
156
|
+
raise ValueError("ALLOWED_EMAIL_SUFFIXES must be a list of strings")
|
|
152
157
|
# If the user email suffix is not in the allowed list, return a 404 response.
|
|
153
|
-
if not any([
|
|
158
|
+
if not any([attributes["email"].lower().endswith(suffix.lower().strip()) for suffix in allowed_email_suffixes]):
|
|
154
159
|
return http.HttpResponseForbidden()
|
|
155
160
|
|
|
156
161
|
# Check for an existing User instance.
|
|
157
|
-
if
|
|
158
|
-
user = User.objects.filter(email__iexact=
|
|
159
|
-
elif User.__name__ != "EmailUser" and User.objects.filter(username__iexact=
|
|
160
|
-
user = User.objects.filter(username__iexact=
|
|
162
|
+
if attributes["email"] and User.objects.filter(email__iexact=attributes["email"]).exists():
|
|
163
|
+
user = User.objects.filter(email__iexact=attributes["email"]).first()
|
|
164
|
+
elif User.__name__ != "EmailUser" and User.objects.filter(username__iexact=attributes["username"]).exists():
|
|
165
|
+
user = User.objects.filter(username__iexact=attributes["username"]).first()
|
|
161
166
|
else:
|
|
162
167
|
user = User(last_login=timezone.localtime())
|
|
163
168
|
|
|
164
169
|
# Set the user's details from the supplied information.
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
user_has_changed = False
|
|
171
|
+
for attr, value in attributes.items():
|
|
172
|
+
if getattr(user, attr) != value:
|
|
173
|
+
setattr(user, attr, value)
|
|
174
|
+
user_has_changed = True
|
|
175
|
+
if user_has_changed:
|
|
176
|
+
user.save()
|
|
177
|
+
|
|
167
178
|
user.backend = "django.contrib.auth.backends.ModelBackend"
|
|
168
179
|
|
|
169
180
|
# 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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|