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.
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/PKG-INFO +12 -17
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/README.md +9 -13
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/pyproject.toml +7 -8
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/src/dbca_utils/middleware.py +26 -16
- dbca_utils-2.1.5/tests/templates/tests/test_model_list.html +17 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/tests.py +53 -5
- dbca_utils-2.1.3/tests/templates/tests/test_model_list.html +0 -12
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/LICENSE +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/src/dbca_utils/__init__.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/src/dbca_utils/models.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/src/dbca_utils/utils.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/__init__.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/apps.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/migrations/0001_initial.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/migrations/__init__.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/models.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/settings.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/urls.py +0 -0
- {dbca_utils-2.1.3 → dbca_utils-2.1.5}/tests/views.py +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbca-utils
|
|
3
|
-
Version: 2.1.
|
|
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.
|
|
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
|
-
##
|
|
35
|
+
## Requirements
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
40
|
+
## Development
|
|
44
41
|
|
|
45
|
-
|
|
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
|
|
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
|
-
##
|
|
5
|
+
## Requirements
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
## Development
|
|
13
11
|
|
|
14
|
-
|
|
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
|
|
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
|
+
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.
|
|
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.
|
|
44
|
-
"pytest-sugar>=1.
|
|
45
|
-
"tox>=4.
|
|
46
|
-
"tox-uv>=1.
|
|
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,
|
|
132
|
-
if
|
|
133
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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([
|
|
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
|
|
155
|
-
user = User.objects.filter(email__iexact=
|
|
156
|
-
elif User.__name__ != "EmailUser" and User.objects.filter(username__iexact=
|
|
157
|
-
user = User.objects.filter(username__iexact=
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
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
|