Skip to content

Commit

Permalink
Added username blocklist plugin.
Browse files Browse the repository at this point in the history
  • Loading branch information
bmispelon authored and felixxm committed Feb 16, 2024
1 parent 31b818a commit 165e4b4
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 3 deletions.
44 changes: 43 additions & 1 deletion DjangoPlugin/tracdjangoplugin/plugins.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from urllib.parse import urlparse

from trac.config import ListOption
from trac.core import Component, implements
from trac.web.chrome import INavigationContributor
from trac.web.api import IRequestFilter, IRequestHandler, RequestDone
from trac.web.auth import LoginModule
from trac.wiki.web_ui import WikiModule
from trac.util.html import tag
from tracext.github import GitHubBrowser
from tracext.github import GitHubLoginModule, GitHubBrowser

from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
Expand Down Expand Up @@ -139,3 +140,44 @@ def _get_safe_redirect_url(self, req):
return redirect_url
else:
return settings.LOGIN_REDIRECT_URL


class ReservedUsernamesComponent(Component):
"""
Prevents some users from logging in on the website. Useful for example to prevent
users whose name clashes with a permission group.
The list of reserved usernames can be configured in trac.ini by specifying
`reserved.usernames` as a comma-separated list under the [djangoplugin] header.
If such a user tries to log in, they will be logged out and redirected to the login
page with a message telling them to choose a different account.
"""

implements(IRequestFilter)

reserved_names = ListOption(
section="djangoplugin",
name="reserved_usernames",
default="authenticated",
doc="A list (comma-separated) of usernames that won't be allowed to log in",
)

def pre_process_request(self, req, handler):
if req.authname in self.reserved_names:
self.force_logout_and_redirect(req)
return handler

def force_logout_and_redirect(self, req):
component = GitHubLoginModule(self.env)
# Trac's builtin LoginModule silently ignores logout requests that aren't POST,
# so we need to be a bit creative here
req.environ["REQUEST_METHOD"] = "POST"
try:
GitHubLoginModule(self.env)._do_logout(req)
except RequestDone:
pass # catch the redirection exception so we can make our own
req.redirect("/login?reserved=%s" % req.authname)

def post_process_request(self, req, template, data, metadata):
return template, data, metadata # required by Trac to exist
37 changes: 35 additions & 2 deletions DjangoPlugin/tracdjangoplugin/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@

from trac.test import EnvironmentStub, MockRequest
from trac.web.api import RequestDone
from trac.web.main import RequestDispatcher

from tracdjangoplugin.middlewares import DjangoDBManagementMiddleware
from tracdjangoplugin.plugins import PlainLoginComponent
from tracdjangoplugin.plugins import PlainLoginComponent, ReservedUsernamesComponent


class PlainLoginComponentTestCase(TestCase):
Expand Down Expand Up @@ -181,3 +180,37 @@ def test_request_finished_fired_even_with_error(self):
with self.assertRaises(ZeroDivisionError):
list(app(None, None))
self.signals[request_finished].assert_called_once()


class ReservedUsernamesComponentTestCase(TestCase):
def setUp(self):
self.env = EnvironmentStub(
config=[("djangoplugin", "reserved_usernames", "invalid")]
)
self.component = ReservedUsernamesComponent(self.env)
self.request_factory = partial(MockRequest, self.env)

def test_reserved_name_redirect(self):
request = self.request_factory(
path_info="/", script_name="", authname="invalid"
)
with self.assertRaises(RequestDone):
self.component.pre_process_request(
request, handler=None
) # handler doesn't matter here

redirect_url = request.headers_sent["Location"]
# Trac's EnvironmentStub uses http://example.org by as a base_url
self.assertEqual(redirect_url, "http://example.org/login?reserved=invalid")

def test_non_reserved_name_goes_through(self):
request = self.request_factory(path_info="/", authname="valid")
handler = object()
retval = self.component.pre_process_request(request, handler=handler)
self.assertIs(retval, handler)

def test_anonymous_goes_through(self):
request = self.request_factory(path_info="/")
handler = object()
retval = self.component.pre_process_request(request, handler=handler)
self.assertIs(retval, handler)
4 changes: 4 additions & 0 deletions trac-env/conf/trac.ini
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ webadmin.plugin.* = enabled
webadmin.ticket.* = enabled
webadmin.web_ui.* = enabled

[djangoplugin]
# "admins" is the name of a group we use
reserved_usernames = authenticated,admins

[github]
branches = main stable/*
client_id = GITHUB_OAUTH_CLIENT_ID
Expand Down
6 changes: 6 additions & 0 deletions trac-env/templates/plainlogin.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
# endblock title

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

<section class="login-github">
Expand Down

0 comments on commit 165e4b4

Please sign in to comment.