plain.auth 0.14.0__tar.gz → 0.16.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.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: plain.auth
3
+ Version: 0.16.0
4
+ Summary: Add users to your app and decide what they can access.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: plain-models<1.0.0
9
+ Requires-Dist: plain-sessions<1.0.0
10
+ Requires-Dist: plain<1.0.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # plain.auth
14
+
15
+ **Add users to your app and decide what they can access.**
16
+
17
+ - [Overview](#overview)
18
+ - [Authentication setup](#authentication-setup)
19
+ - [Settings configuration](#settings-configuration)
20
+ - [Creating a user model](#creating-a-user-model)
21
+ - [Login views](#login-views)
22
+ - [Checking if a user is logged in](#checking-if-a-user-is-logged-in)
23
+ - [Restricting views](#restricting-views)
24
+ - [Installation](#installation)
25
+
26
+ ## Overview
27
+
28
+ The `plain.auth` package provides user authentication and authorization for Plain applications. Here's a basic example of checking if a user is logged in:
29
+
30
+ ```python
31
+ # In a view
32
+ if request.user:
33
+ print(f"Hello, {request.user.email}!")
34
+ else:
35
+ print("You are not logged in.")
36
+ ```
37
+
38
+ And restricting a view to logged-in users:
39
+
40
+ ```python
41
+ from plain.auth.views import AuthViewMixin
42
+ from plain.views import View
43
+
44
+ class ProfileView(AuthViewMixin, View):
45
+ login_required = True
46
+
47
+ def get(self):
48
+ return f"Welcome, {self.request.user.email}!"
49
+ ```
50
+
51
+ ## Authentication setup
52
+
53
+ ### Settings configuration
54
+
55
+ Configure your authentication settings in `app/settings.py`:
56
+
57
+ ```python
58
+ INSTALLED_PACKAGES = [
59
+ # ...
60
+ "plain.auth",
61
+ "plain.sessions",
62
+ "plain.passwords", # Or another auth method
63
+ ]
64
+
65
+ MIDDLEWARE = [
66
+ "plain.sessions.middleware.SessionMiddleware",
67
+ "plain.auth.middleware.AuthenticationMiddleware",
68
+ ]
69
+
70
+ AUTH_USER_MODEL = "users.User"
71
+ AUTH_LOGIN_URL = "login"
72
+ ```
73
+
74
+ ### Creating a user model
75
+
76
+ Create your own user model using `plain create users` or manually:
77
+
78
+ ```python
79
+ # app/users/models.py
80
+ from plain import models
81
+ from plain.passwords.models import PasswordField
82
+
83
+
84
+ class User(models.Model):
85
+ email = models.EmailField()
86
+ password = PasswordField()
87
+ is_admin = models.BooleanField(default=False)
88
+ created_at = models.DateTimeField(auto_now_add=True)
89
+
90
+ def __str__(self):
91
+ return self.email
92
+ ```
93
+
94
+ ### Login views
95
+
96
+ To log users in, you'll need to pair this package with an authentication method:
97
+
98
+ - `plain-passwords` - Username/password authentication
99
+ - `plain-oauth` - OAuth provider authentication
100
+ - `plain-passkeys` (TBD) - WebAuthn/passkey authentication
101
+ - `plain-passlinks` (TBD) - Magic link authentication
102
+
103
+ Example with password authentication:
104
+
105
+ ```python
106
+ # app/urls.py
107
+ from plain.auth.views import LogoutView
108
+ from plain.urls import path
109
+ from plain.passwords.views import PasswordLoginView
110
+
111
+
112
+ class LoginView(PasswordLoginView):
113
+ template_name = "login.html"
114
+
115
+
116
+ urlpatterns = [
117
+ path("logout/", LogoutView, name="logout"),
118
+ path("login/", LoginView, name="login"),
119
+ ]
120
+ ```
121
+
122
+ ## Checking if a user is logged in
123
+
124
+ A `request.user` will either be `None` or point to an instance of your `AUTH_USER_MODEL`.
125
+
126
+ In templates:
127
+
128
+ ```html
129
+ {% if request.user %}
130
+ <p>Hello, {{ request.user.email }}!</p>
131
+ {% else %}
132
+ <p>You are not logged in.</p>
133
+ {% endif %}
134
+ ```
135
+
136
+ In Python code:
137
+
138
+ ```python
139
+ if request.user:
140
+ print(f"Hello, {request.user.email}!")
141
+ else:
142
+ print("You are not logged in.")
143
+ ```
144
+
145
+ ## Restricting views
146
+
147
+ Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
148
+
149
+ ```python
150
+ from plain.auth.views import AuthViewMixin
151
+ from plain.exceptions import PermissionDenied
152
+ from plain.views import View
153
+
154
+
155
+ class LoggedInView(AuthViewMixin, View):
156
+ login_required = True
157
+
158
+
159
+ class AdminOnlyView(AuthViewMixin, View):
160
+ login_required = True
161
+ admin_required = True
162
+
163
+
164
+ class CustomPermissionView(AuthViewMixin, View):
165
+ def check_auth(self):
166
+ super().check_auth()
167
+ if not self.request.user.is_special:
168
+ raise PermissionDenied("You're not special!")
169
+ ```
170
+
171
+ The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
172
+
173
+ - `login_required` - Requires a logged-in user
174
+ - `admin_required` - Requires `user.is_admin` to be True
175
+ - `check_auth()` - Override for custom authorization logic
176
+
177
+ ## Installation
178
+
179
+ Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
180
+
181
+ ```bash
182
+ uv add plain.auth
183
+ ```
@@ -0,0 +1,43 @@
1
+ # plain-auth changelog
2
+
3
+ ## [0.16.0](https://github.com/dropseed/plain/releases/plain-auth@0.16.0) (2025-08-19)
4
+
5
+ ### What's changed
6
+
7
+ - Removed automatic CSRF token rotation on login as part of CSRF system refactor using Sec-Fetch-Site headers ([9551508](https://github.com/dropseed/plain/commit/955150800c))
8
+ - Updated README with improved documentation, examples, and better package description ([4ebecd1](https://github.com/dropseed/plain/commit/4ebecd1856))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
14
+ ## [0.15.0](https://github.com/dropseed/plain/releases/plain-auth@0.15.0) (2025-07-22)
15
+
16
+ ### What's changed
17
+
18
+ - Replaced `pk` field references with `id` field references in session management ([4b8fa6a](https://github.com/dropseed/plain/commit/4b8fa6aef1))
19
+ - Simplified user ID handling in sessions by using direct integer storage instead of field serialization ([4b8fa6a](https://github.com/dropseed/plain/commit/4b8fa6aef1))
20
+
21
+ ### Upgrade instructions
22
+
23
+ - No changes required
24
+
25
+ ## [0.14.0](https://github.com/dropseed/plain/releases/plain-auth@0.14.0) (2025-07-18)
26
+
27
+ ### What's changed
28
+
29
+ - Added OpenTelemetry tracing support with automatic user ID attribute setting in auth middleware ([b0224d0](https://github.com/dropseed/plain/commit/b0224d0418))
30
+
31
+ ### Upgrade instructions
32
+
33
+ - No changes required
34
+
35
+ ## [0.13.0](https://github.com/dropseed/plain/releases/plain-auth@0.13.0) (2025-06-23)
36
+
37
+ ### What's changed
38
+
39
+ - 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)).
40
+
41
+ ### Upgrade instructions
42
+
43
+ - No changes required
@@ -0,0 +1,171 @@
1
+ # plain.auth
2
+
3
+ **Add users to your app and decide what they can access.**
4
+
5
+ - [Overview](#overview)
6
+ - [Authentication setup](#authentication-setup)
7
+ - [Settings configuration](#settings-configuration)
8
+ - [Creating a user model](#creating-a-user-model)
9
+ - [Login views](#login-views)
10
+ - [Checking if a user is logged in](#checking-if-a-user-is-logged-in)
11
+ - [Restricting views](#restricting-views)
12
+ - [Installation](#installation)
13
+
14
+ ## Overview
15
+
16
+ The `plain.auth` package provides user authentication and authorization for Plain applications. Here's a basic example of checking if a user is logged in:
17
+
18
+ ```python
19
+ # In a view
20
+ if request.user:
21
+ print(f"Hello, {request.user.email}!")
22
+ else:
23
+ print("You are not logged in.")
24
+ ```
25
+
26
+ And restricting a view to logged-in users:
27
+
28
+ ```python
29
+ from plain.auth.views import AuthViewMixin
30
+ from plain.views import View
31
+
32
+ class ProfileView(AuthViewMixin, View):
33
+ login_required = True
34
+
35
+ def get(self):
36
+ return f"Welcome, {self.request.user.email}!"
37
+ ```
38
+
39
+ ## Authentication setup
40
+
41
+ ### Settings configuration
42
+
43
+ Configure your authentication settings in `app/settings.py`:
44
+
45
+ ```python
46
+ INSTALLED_PACKAGES = [
47
+ # ...
48
+ "plain.auth",
49
+ "plain.sessions",
50
+ "plain.passwords", # Or another auth method
51
+ ]
52
+
53
+ MIDDLEWARE = [
54
+ "plain.sessions.middleware.SessionMiddleware",
55
+ "plain.auth.middleware.AuthenticationMiddleware",
56
+ ]
57
+
58
+ AUTH_USER_MODEL = "users.User"
59
+ AUTH_LOGIN_URL = "login"
60
+ ```
61
+
62
+ ### Creating a user model
63
+
64
+ Create your own user model using `plain create users` or manually:
65
+
66
+ ```python
67
+ # app/users/models.py
68
+ from plain import models
69
+ from plain.passwords.models import PasswordField
70
+
71
+
72
+ class User(models.Model):
73
+ email = models.EmailField()
74
+ password = PasswordField()
75
+ is_admin = models.BooleanField(default=False)
76
+ created_at = models.DateTimeField(auto_now_add=True)
77
+
78
+ def __str__(self):
79
+ return self.email
80
+ ```
81
+
82
+ ### Login views
83
+
84
+ To log users in, you'll need to pair this package with an authentication method:
85
+
86
+ - `plain-passwords` - Username/password authentication
87
+ - `plain-oauth` - OAuth provider authentication
88
+ - `plain-passkeys` (TBD) - WebAuthn/passkey authentication
89
+ - `plain-passlinks` (TBD) - Magic link authentication
90
+
91
+ Example with password authentication:
92
+
93
+ ```python
94
+ # app/urls.py
95
+ from plain.auth.views import LogoutView
96
+ from plain.urls import path
97
+ from plain.passwords.views import PasswordLoginView
98
+
99
+
100
+ class LoginView(PasswordLoginView):
101
+ template_name = "login.html"
102
+
103
+
104
+ urlpatterns = [
105
+ path("logout/", LogoutView, name="logout"),
106
+ path("login/", LoginView, name="login"),
107
+ ]
108
+ ```
109
+
110
+ ## Checking if a user is logged in
111
+
112
+ A `request.user` will either be `None` or point to an instance of your `AUTH_USER_MODEL`.
113
+
114
+ In templates:
115
+
116
+ ```html
117
+ {% if request.user %}
118
+ <p>Hello, {{ request.user.email }}!</p>
119
+ {% else %}
120
+ <p>You are not logged in.</p>
121
+ {% endif %}
122
+ ```
123
+
124
+ In Python code:
125
+
126
+ ```python
127
+ if request.user:
128
+ print(f"Hello, {request.user.email}!")
129
+ else:
130
+ print("You are not logged in.")
131
+ ```
132
+
133
+ ## Restricting views
134
+
135
+ Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
136
+
137
+ ```python
138
+ from plain.auth.views import AuthViewMixin
139
+ from plain.exceptions import PermissionDenied
140
+ from plain.views import View
141
+
142
+
143
+ class LoggedInView(AuthViewMixin, View):
144
+ login_required = True
145
+
146
+
147
+ class AdminOnlyView(AuthViewMixin, View):
148
+ login_required = True
149
+ admin_required = True
150
+
151
+
152
+ class CustomPermissionView(AuthViewMixin, View):
153
+ def check_auth(self):
154
+ super().check_auth()
155
+ if not self.request.user.is_special:
156
+ raise PermissionDenied("You're not special!")
157
+ ```
158
+
159
+ The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
160
+
161
+ - `login_required` - Requires a logged-in user
162
+ - `admin_required` - Requires `user.is_admin` to be True
163
+ - `check_auth()` - Override for custom authorization logic
164
+
165
+ ## Installation
166
+
167
+ Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
168
+
169
+ ```bash
170
+ uv add plain.auth
171
+ ```
@@ -1,4 +1,3 @@
1
- from plain.csrf.middleware import rotate_token
2
1
  from plain.exceptions import ImproperlyConfigured
3
2
  from plain.models import models_registry
4
3
  from plain.runtime import settings
@@ -8,12 +7,6 @@ USER_ID_SESSION_KEY = "_auth_user_id"
8
7
  USER_HASH_SESSION_KEY = "_auth_user_hash"
9
8
 
10
9
 
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
10
  def get_session_auth_hash(user):
18
11
  """
19
12
  Return an HMAC of the password field.
@@ -62,7 +55,7 @@ def login(request, user):
62
55
  session_auth_hash = ""
63
56
 
64
57
  if USER_ID_SESSION_KEY in request.session:
65
- if _get_user_id_from_session(request) != user.pk:
58
+ if int(request.session[USER_ID_SESSION_KEY]) != user.id:
66
59
  # To avoid reusing another user's session, create a new, empty
67
60
  # session if the existing session corresponds to a different
68
61
  # authenticated user.
@@ -78,11 +71,10 @@ def login(request, user):
78
71
  # typically done after user login to prevent session fixation attacks.
79
72
  request.session.cycle_key()
80
73
 
81
- request.session[USER_ID_SESSION_KEY] = user._meta.pk.value_to_string(user)
74
+ request.session[USER_ID_SESSION_KEY] = user.id
82
75
  request.session[USER_HASH_SESSION_KEY] = session_auth_hash
83
76
  if hasattr(request, "user"):
84
77
  request.user = user
85
- rotate_token(request)
86
78
 
87
79
 
88
80
  def logout(request):
@@ -121,11 +113,9 @@ def get_user(request):
121
113
  if USER_ID_SESSION_KEY not in request.session:
122
114
  return None
123
115
 
124
- user_id = _get_user_id_from_session(request)
125
-
126
116
  UserModel = get_user_model()
127
117
  try:
128
- user = UserModel._default_manager.get(pk=user_id)
118
+ user = UserModel._default_manager.get(id=request.session[USER_ID_SESSION_KEY])
129
119
  except UserModel.DoesNotExist:
130
120
  return None
131
121
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "plain.auth"
3
- version = "0.14.0"
4
- description = "User authentication and authorization for Plain."
3
+ version = "0.16.0"
4
+ description = "Add users to your app and decide what they can access."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -13,7 +13,7 @@ class Migration(migrations.Migration):
13
13
  migrations.CreateModel(
14
14
  name="User",
15
15
  fields=[
16
- ("id", models.BigAutoField(auto_created=True, primary_key=True)),
16
+ ("id", models.PrimaryKeyField()),
17
17
  ("username", models.CharField(max_length=255)),
18
18
  ("is_admin", models.BooleanField(default=False)),
19
19
  ],
@@ -1,128 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: plain.auth
3
- Version: 0.14.0
4
- Summary: User authentication and authorization for Plain.
5
- Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
- License-File: LICENSE
7
- Requires-Python: >=3.11
8
- Requires-Dist: plain-models<1.0.0
9
- Requires-Dist: plain-sessions<1.0.0
10
- Requires-Dist: plain<1.0.0
11
- Description-Content-Type: text/markdown
12
-
13
- # plain.auth
14
-
15
- Add users to your app and define which views they can access.
16
-
17
- To log a user in, you'll want to pair this package with:
18
-
19
- - `plain-passwords`
20
- - `plain-oauth`
21
- - `plain-passkeys` (TBD)
22
- - `plain-passlinks` (TBD)
23
-
24
- ## Installation
25
-
26
- ```python
27
- # app/settings.py
28
- INSTALLED_PACKAGES = [
29
- # ...
30
- "plain.auth",
31
- "plain.sessions",
32
- "plain.passwords",
33
- ]
34
-
35
- MIDDLEWARE = [
36
- "plain.sessions.middleware.SessionMiddleware",
37
- "plain.auth.middleware.AuthenticationMiddleware",
38
- ]
39
-
40
- AUTH_USER_MODEL = "users.User"
41
- AUTH_LOGIN_URL = "login"
42
- ```
43
-
44
- Create your own user model (`plain create users`).
45
-
46
- ```python
47
- # app/users/models.py
48
- from plain import models
49
- from plain.passwords.models import PasswordField
50
-
51
-
52
- class User(models.Model):
53
- email = models.EmailField()
54
- password = PasswordField()
55
- is_admin = models.BooleanField(default=False)
56
- created_at = models.DateTimeField(auto_now_add=True)
57
-
58
- def __str__(self):
59
- return self.email
60
- ```
61
-
62
- Define your URL/view where users can log in.
63
-
64
- ```python
65
- # app/urls.py
66
- from plain.auth.views import LoginView, LogoutView
67
- from plain.urls import include, path
68
- from plain.passwords.views import PasswordLoginView
69
-
70
-
71
- class LoginView(PasswordLoginView):
72
- template_name = "login.html"
73
-
74
-
75
- urlpatterns = [
76
- path("logout/", LogoutView, name="logout"),
77
- path("login/", LoginView, name="login"),
78
- ]
79
- ```
80
-
81
- ## Checking if a user is logged in
82
-
83
- A `request.user` will either be `None` or point to an instance of a your `AUTH_USER_MODEL`.
84
-
85
- So in templates you can do:
86
-
87
- ```html
88
- {% if request.user %}
89
- <p>Hello, {{ request.user.email }}!</p>
90
- {% else %}
91
- <p>You are not logged in.</p>
92
- {% endif %}
93
- ```
94
-
95
- Or in Python:
96
-
97
- ```python
98
- if request.user:
99
- print(f"Hello, {request.user.email}!")
100
- else:
101
- print("You are not logged in.")
102
- ```
103
-
104
- ## Restricting views
105
-
106
- Use the `AuthViewMixin` to restrict views to logged in users, admin users, or custom logic.
107
-
108
- ```python
109
- from plain.auth.views import AuthViewMixin
110
- from plain.exceptions import PermissionDenied
111
- from plain.views import View
112
-
113
-
114
- class LoggedInView(AuthViewMixin, View):
115
- login_required = True
116
-
117
-
118
- class AdminOnlyView(AuthViewMixin, View):
119
- login_required = True
120
- admin_required = True
121
-
122
-
123
- class CustomPermissionView(AuthViewMixin, View):
124
- def check_auth(self):
125
- super().check_auth()
126
- if not self.request.user.is_special:
127
- raise PermissionDenied("You're not special!")
128
- ```
@@ -1,21 +0,0 @@
1
- # plain-auth changelog
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
-
13
- ## [0.13.0](https://github.com/dropseed/plain/releases/plain-auth@0.13.0) (2025-06-23)
14
-
15
- ### What's changed
16
-
17
- - 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)).
18
-
19
- ### Upgrade instructions
20
-
21
- - No changes required
@@ -1,116 +0,0 @@
1
- # plain.auth
2
-
3
- Add users to your app and define which views they can access.
4
-
5
- To log a user in, you'll want to pair this package with:
6
-
7
- - `plain-passwords`
8
- - `plain-oauth`
9
- - `plain-passkeys` (TBD)
10
- - `plain-passlinks` (TBD)
11
-
12
- ## Installation
13
-
14
- ```python
15
- # app/settings.py
16
- INSTALLED_PACKAGES = [
17
- # ...
18
- "plain.auth",
19
- "plain.sessions",
20
- "plain.passwords",
21
- ]
22
-
23
- MIDDLEWARE = [
24
- "plain.sessions.middleware.SessionMiddleware",
25
- "plain.auth.middleware.AuthenticationMiddleware",
26
- ]
27
-
28
- AUTH_USER_MODEL = "users.User"
29
- AUTH_LOGIN_URL = "login"
30
- ```
31
-
32
- Create your own user model (`plain create users`).
33
-
34
- ```python
35
- # app/users/models.py
36
- from plain import models
37
- from plain.passwords.models import PasswordField
38
-
39
-
40
- class User(models.Model):
41
- email = models.EmailField()
42
- password = PasswordField()
43
- is_admin = models.BooleanField(default=False)
44
- created_at = models.DateTimeField(auto_now_add=True)
45
-
46
- def __str__(self):
47
- return self.email
48
- ```
49
-
50
- Define your URL/view where users can log in.
51
-
52
- ```python
53
- # app/urls.py
54
- from plain.auth.views import LoginView, LogoutView
55
- from plain.urls import include, path
56
- from plain.passwords.views import PasswordLoginView
57
-
58
-
59
- class LoginView(PasswordLoginView):
60
- template_name = "login.html"
61
-
62
-
63
- urlpatterns = [
64
- path("logout/", LogoutView, name="logout"),
65
- path("login/", LoginView, name="login"),
66
- ]
67
- ```
68
-
69
- ## Checking if a user is logged in
70
-
71
- A `request.user` will either be `None` or point to an instance of a your `AUTH_USER_MODEL`.
72
-
73
- So in templates you can do:
74
-
75
- ```html
76
- {% if request.user %}
77
- <p>Hello, {{ request.user.email }}!</p>
78
- {% else %}
79
- <p>You are not logged in.</p>
80
- {% endif %}
81
- ```
82
-
83
- Or in Python:
84
-
85
- ```python
86
- if request.user:
87
- print(f"Hello, {request.user.email}!")
88
- else:
89
- print("You are not logged in.")
90
- ```
91
-
92
- ## Restricting views
93
-
94
- Use the `AuthViewMixin` to restrict views to logged in users, admin users, or custom logic.
95
-
96
- ```python
97
- from plain.auth.views import AuthViewMixin
98
- from plain.exceptions import PermissionDenied
99
- from plain.views import View
100
-
101
-
102
- class LoggedInView(AuthViewMixin, View):
103
- login_required = True
104
-
105
-
106
- class AdminOnlyView(AuthViewMixin, View):
107
- login_required = True
108
- admin_required = True
109
-
110
-
111
- class CustomPermissionView(AuthViewMixin, View):
112
- def check_auth(self):
113
- super().check_auth()
114
- if not self.request.user.is_special:
115
- raise PermissionDenied("You're not special!")
116
- ```
File without changes
File without changes
File without changes