plain.auth 0.13.0__tar.gz → 0.15.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.
@@ -4,7 +4,6 @@
4
4
  *.py[co]
5
5
  __pycache__
6
6
  *.DS_Store
7
- .coverage
8
7
 
9
8
  # Test apps
10
9
  plain*/tests/.plain
@@ -16,3 +15,5 @@ plain*/tests/.plain
16
15
 
17
16
  # Plain temp dirs
18
17
  .plain
18
+
19
+ .vscode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.auth
3
- Version: 0.13.0
3
+ Version: 0.15.0
4
4
  Summary: User authentication and authorization for Plain.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -0,0 +1,32 @@
1
+ # plain-auth changelog
2
+
3
+ ## [0.15.0](https://github.com/dropseed/plain/releases/plain-auth@0.15.0) (2025-07-22)
4
+
5
+ ### What's changed
6
+
7
+ - Replaced `pk` field references with `id` field references in session management ([4b8fa6a](https://github.com/dropseed/plain/commit/4b8fa6aef1))
8
+ - Simplified user ID handling in sessions by using direct integer storage instead of field serialization ([4b8fa6a](https://github.com/dropseed/plain/commit/4b8fa6aef1))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
14
+ ## [0.14.0](https://github.com/dropseed/plain/releases/plain-auth@0.14.0) (2025-07-18)
15
+
16
+ ### What's changed
17
+
18
+ - Added OpenTelemetry tracing support with automatic user ID attribute setting in auth middleware ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
19
+
20
+ ### Upgrade instructions
21
+
22
+ - No changes required
23
+
24
+ ## [0.13.0](https://github.com/dropseed/plain/releases/plain-auth@0.13.0) (2025-06-23)
25
+
26
+ ### What's changed
27
+
28
+ - Added `login_client` and `logout_client` helpers to `plain.auth.test` for easily logging users in and out of the Django test client ([eb8a023](https://github.com/dropseed/plain/commit/eb8a023)).
29
+
30
+ ### Upgrade instructions
31
+
32
+ - No changes required
@@ -1,3 +1,6 @@
1
+ from opentelemetry import trace
2
+ from opentelemetry.semconv._incubating.attributes.user_attributes import USER_ID
3
+
1
4
  from plain import auth
2
5
  from plain.exceptions import ImproperlyConfigured
3
6
  from plain.utils.functional import SimpleLazyObject
@@ -6,6 +9,8 @@ from plain.utils.functional import SimpleLazyObject
6
9
  def get_user(request):
7
10
  if not hasattr(request, "_cached_user"):
8
11
  request._cached_user = auth.get_user(request)
12
+ if request._cached_user:
13
+ trace.get_current_span().set_attribute(USER_ID, request._cached_user.id)
9
14
  return request._cached_user
10
15
 
11
16
 
@@ -8,12 +8,6 @@ USER_ID_SESSION_KEY = "_auth_user_id"
8
8
  USER_HASH_SESSION_KEY = "_auth_user_hash"
9
9
 
10
10
 
11
- def _get_user_id_from_session(request):
12
- # This value in the session is always serialized to a string, so we need
13
- # to convert it back to Python whenever we access it.
14
- return get_user_model()._meta.pk.to_python(request.session[USER_ID_SESSION_KEY])
15
-
16
-
17
11
  def get_session_auth_hash(user):
18
12
  """
19
13
  Return an HMAC of the password field.
@@ -62,7 +56,7 @@ def login(request, user):
62
56
  session_auth_hash = ""
63
57
 
64
58
  if USER_ID_SESSION_KEY in request.session:
65
- if _get_user_id_from_session(request) != user.pk:
59
+ if int(request.session[USER_ID_SESSION_KEY]) != user.id:
66
60
  # To avoid reusing another user's session, create a new, empty
67
61
  # session if the existing session corresponds to a different
68
62
  # authenticated user.
@@ -78,7 +72,7 @@ def login(request, user):
78
72
  # typically done after user login to prevent session fixation attacks.
79
73
  request.session.cycle_key()
80
74
 
81
- request.session[USER_ID_SESSION_KEY] = user._meta.pk.value_to_string(user)
75
+ request.session[USER_ID_SESSION_KEY] = user.id
82
76
  request.session[USER_HASH_SESSION_KEY] = session_auth_hash
83
77
  if hasattr(request, "user"):
84
78
  request.user = user
@@ -121,11 +115,9 @@ def get_user(request):
121
115
  if USER_ID_SESSION_KEY not in request.session:
122
116
  return None
123
117
 
124
- user_id = _get_user_id_from_session(request)
125
-
126
118
  UserModel = get_user_model()
127
119
  try:
128
- user = UserModel._default_manager.get(pk=user_id)
120
+ user = UserModel._default_manager.get(id=request.session[USER_ID_SESSION_KEY])
129
121
  except UserModel.DoesNotExist:
130
122
  return None
131
123
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.auth"
3
- version = "0.13.0"
3
+ version = "0.15.0"
4
4
  description = "User authentication and authorization for Plain."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
@@ -13,6 +13,11 @@ dependencies = [
13
13
  "plain.sessions<1.0.0",
14
14
  ]
15
15
 
16
+ [tool.uv]
17
+ dev-dependencies = [
18
+ "plain.pytest<1.0.0",
19
+ ]
20
+
16
21
  [tool.hatch.build.targets.wheel]
17
22
  packages = ["plain"]
18
23
 
@@ -0,0 +1,14 @@
1
+ SECRET_KEY = "test"
2
+ URLS_ROUTER = "app.urls.AppRouter"
3
+ INSTALLED_PACKAGES = [
4
+ "plain.auth",
5
+ "plain.sessions",
6
+ "plain.models",
7
+ "app.users",
8
+ ]
9
+ MIDDLEWARE = [
10
+ "plain.sessions.middleware.SessionMiddleware",
11
+ "plain.auth.middleware.AuthenticationMiddleware",
12
+ ]
13
+ AUTH_LOGIN_URL = "login"
14
+ AUTH_USER_MODEL = "users.User"
@@ -0,0 +1,45 @@
1
+ from plain.auth.views import AuthViewMixin
2
+ from plain.urls import Router, path
3
+ from plain.views import View
4
+
5
+
6
+ class LoginView(View):
7
+ def get(self):
8
+ return "login"
9
+
10
+
11
+ class ProtectedView(AuthViewMixin, View):
12
+ def get(self):
13
+ return "protected"
14
+
15
+
16
+ class OpenView(AuthViewMixin, View):
17
+ login_required = False
18
+
19
+ def get(self):
20
+ return "open"
21
+
22
+
23
+ class AdminView(AuthViewMixin, View):
24
+ admin_required = True
25
+
26
+ def get(self):
27
+ return "admin"
28
+
29
+
30
+ class NoLoginUrlView(AuthViewMixin, View):
31
+ login_url = None
32
+
33
+ def get(self):
34
+ return "none"
35
+
36
+
37
+ class AppRouter(Router):
38
+ namespace = ""
39
+ urls = [
40
+ path("login/", LoginView, name="login"),
41
+ path("protected/", ProtectedView, name="protected"),
42
+ path("open/", OpenView, name="open"),
43
+ path("admin/", AdminView, name="admin"),
44
+ path("nolink/", NoLoginUrlView, name="nolink"),
45
+ ]
@@ -0,0 +1,21 @@
1
+ # Generated by Plain 0.21.5 on 2025-02-17 04:12
2
+
3
+ from plain import models
4
+ from plain.models import migrations
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ initial = True
9
+
10
+ dependencies = []
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name="User",
15
+ fields=[
16
+ ("id", models.PrimaryKeyField()),
17
+ ("username", models.CharField(max_length=255)),
18
+ ("is_admin", models.BooleanField(default=False)),
19
+ ],
20
+ ),
21
+ ]
@@ -0,0 +1,7 @@
1
+ from plain import models
2
+
3
+
4
+ @models.register_model
5
+ class User(models.Model):
6
+ username = models.CharField(max_length=255)
7
+ is_admin = models.BooleanField(default=False)
@@ -0,0 +1,41 @@
1
+ from plain.auth import get_user_model
2
+ from plain.test import Client
3
+
4
+
5
+ def test_login_required_redirect(db):
6
+ client = Client()
7
+ response = client.get("/protected/")
8
+ assert response.status_code == 302
9
+ assert response.url == "/login/?next=/protected/"
10
+
11
+
12
+ def test_view_without_login_required(db):
13
+ client = Client()
14
+ response = client.get("/open/")
15
+ assert response.status_code == 200
16
+ assert response.content == b"open"
17
+ assert response.headers["Cache-Control"] == "private"
18
+
19
+
20
+ def test_admin_required(db):
21
+ client = Client()
22
+ # login required first
23
+ assert client.get("/admin/").status_code == 302
24
+
25
+ user = get_user_model().objects.create(username="user")
26
+ client.force_login(user)
27
+ # not admin -> 404
28
+ assert client.get("/admin/").status_code == 404
29
+
30
+ user.is_admin = True
31
+ user.save()
32
+ # now admin -> success
33
+ resp = client.get("/admin/")
34
+ assert resp.status_code == 200
35
+ assert resp.content == b"admin"
36
+
37
+
38
+ def test_no_login_url_forbidden(db):
39
+ client = Client()
40
+ response = client.get("/nolink/")
41
+ assert response.status_code == 403
@@ -1,11 +0,0 @@
1
- # plain-auth changelog
2
-
3
- ## [0.13.0](https://github.com/dropseed/plain/releases/plain-auth@0.13.0) (2025-06-23)
4
-
5
- ### What's changed
6
-
7
- - Added `login_client` and `logout_client` helpers to `plain.auth.test` for easily logging users in and out of the Django test client ([eb8a023](https://github.com/dropseed/plain/commit/eb8a023)).
8
-
9
- ### Upgrade instructions
10
-
11
- - No changes required
File without changes
File without changes