Skip to content

Commit 165e4b4

Browse files
bmispelonfelixxm
authored andcommitted
Added username blocklist plugin.
1 parent 31b818a commit 165e4b4

File tree

4 files changed

+88
-3
lines changed

4 files changed

+88
-3
lines changed

DjangoPlugin/tracdjangoplugin/plugins.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from urllib.parse import urlparse
22

3+
from trac.config import ListOption
34
from trac.core import Component, implements
45
from trac.web.chrome import INavigationContributor
56
from trac.web.api import IRequestFilter, IRequestHandler, RequestDone
67
from trac.web.auth import LoginModule
78
from trac.wiki.web_ui import UncycloModule
89
from trac.util.html import tag
9-
from tracext.github import GitHubBrowser
10+
from tracext.github import GitHubLoginModule, GitHubBrowser
1011

1112
from django.conf import settings
1213
from django.contrib.auth.forms import AuthenticationForm
@@ -139,3 +140,44 @@ def _get_safe_redirect_url(self, req):
139140
return redirect_url
140141
else:
141142
return settings.LOGIN_REDIRECT_URL
143+
144+
145+
class ReservedUsernamesComponent(Component):
146+
"""
147+
Prevents some users from logging in on the website. Useful for example to prevent
148+
users whose name clashes with a permission group.
149+
150+
The list of reserved usernames can be configured in trac.ini by specifying
151+
`reserved.usernames` as a comma-separated list under the [djangoplugin] header.
152+
153+
If such a user tries to log in, they will be logged out and redirected to the login
154+
page with a message telling them to choose a different account.
155+
"""
156+
157+
implements(IRequestFilter)
158+
159+
reserved_names = ListOption(
160+
section="djangoplugin",
161+
name="reserved_usernames",
162+
default="authenticated",
163+
doc="A list (comma-separated) of usernames that won't be allowed to log in",
164+
)
165+
166+
def pre_process_request(self, req, handler):
167+
if req.authname in self.reserved_names:
168+
self.force_logout_and_redirect(req)
169+
return handler
170+
171+
def force_logout_and_redirect(self, req):
172+
component = GitHubLoginModule(self.env)
173+
# Trac's builtin LoginModule silently ignores logout requests that aren't POST,
174+
# so we need to be a bit creative here
175+
req.environ["REQUEST_METHOD"] = "POST"
176+
try:
177+
GitHubLoginModule(self.env)._do_logout(req)
178+
except RequestDone:
179+
pass # catch the redirection exception so we can make our own
180+
req.redirect("/login?reserved=%s" % req.authname)
181+
182+
def post_process_request(self, req, template, data, metadata):
183+
return template, data, metadata # required by Trac to exist

DjangoPlugin/tracdjangoplugin/tests.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99

1010
from trac.test import EnvironmentStub, MockRequest
1111
from trac.web.api import RequestDone
12-
from trac.web.main import RequestDispatcher
1312

1413
from tracdjangoplugin.middlewares import DjangoDBManagementMiddleware
15-
from tracdjangoplugin.plugins import PlainLoginComponent
14+
from tracdjangoplugin.plugins import PlainLoginComponent, ReservedUsernamesComponent
1615

1716

1817
class PlainLoginComponentTestCase(TestCase):
@@ -181,3 +180,37 @@ def test_request_finished_fired_even_with_error(self):
181180
with self.assertRaises(ZeroDivisionError):
182181
list(app(None, None))
183182
self.signals[request_finished].assert_called_once()
183+
184+
185+
class ReservedUsernamesComponentTestCase(TestCase):
186+
def setUp(self):
187+
self.env = EnvironmentStub(
188+
config=[("djangoplugin", "reserved_usernames", "invalid")]
189+
)
190+
self.component = ReservedUsernamesComponent(self.env)
191+
self.request_factory = partial(MockRequest, self.env)
192+
193+
def test_reserved_name_redirect(self):
194+
request = self.request_factory(
195+
path_info="/", script_name="", authname="invalid"
196+
)
197+
with self.assertRaises(RequestDone):
198+
self.component.pre_process_request(
199+
request, handler=None
200+
) # handler doesn't matter here
201+
202+
redirect_url = request.headers_sent["Location"]
203+
# Trac's EnvironmentStub uses http://example.org by as a base_url
204+
self.assertEqual(redirect_url, "http://example.org/login?reserved=invalid")
205+
206+
def test_non_reserved_name_goes_through(self):
207+
request = self.request_factory(path_info="/", authname="valid")
208+
handler = object()
209+
retval = self.component.pre_process_request(request, handler=handler)
210+
self.assertIs(retval, handler)
211+
212+
def test_anonymous_goes_through(self):
213+
request = self.request_factory(path_info="/")
214+
handler = object()
215+
retval = self.component.pre_process_request(request, handler=handler)
216+
self.assertIs(retval, handler)

trac-env/conf/trac.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ webadmin.plugin.* = enabled
5959
webadmin.ticket.* = enabled
6060
webadmin.web_ui.* = enabled
6161

62+
[djangoplugin]
63+
# "admins" is the name of a group we use
64+
reserved_usernames = authenticated,admins
65+
6266
[github]
6367
branches = main stable/*
6468
client_id = GITHUB_OAUTH_CLIENT_ID

trac-env/templates/plainlogin.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
# endblock title
66

77
# block content
8+
# if 'reserved' in req.args
9+
<div class="system-message">
10+
Sorry, but the username <strong>${req.args.reserved}</strong> is reserved on this website.
11+
Please log in with a different account.
12+
</div>
13+
# endif
814
<h1>Choose how you want to log in</h1>
915

1016
<section class="login-github">

0 commit comments

Comments
 (0)