plain.auth 0.13.0__tar.gz → 0.14.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.14.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
@@ -1,5 +1,15 @@
1
1
  # plain-auth changelog
2
2
 
3
+ ## [0.14.0](https://github.com/dropseed/plain/releases/plain-auth@0.14.0) (2025-07-18)
4
+
5
+ ### What's changed
6
+
7
+ - Added OpenTelemetry tracing support with automatic user ID attribute setting in auth middleware ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required
12
+
3
13
  ## [0.13.0](https://github.com/dropseed/plain/releases/plain-auth@0.13.0) (2025-06-23)
4
14
 
5
15
  ### What's changed
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.auth"
3
- version = "0.13.0"
3
+ version = "0.14.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.BigAutoField(auto_created=True, primary_key=True)),
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
File without changes
File without changes