diff --git a/DjangoPlugin/tracdjangoplugin/__init__.py b/DjangoPlugin/tracdjangoplugin/__init__.py index 52a08f3..d36a981 100644 --- a/DjangoPlugin/tracdjangoplugin/__init__.py +++ b/DjangoPlugin/tracdjangoplugin/__init__.py @@ -1,6 +1,7 @@ from trac.core import Component, implements from trac.web.chrome import INavigationContributor -from trac.web.api import IRequestFilter, IRequestHandler +from trac.web.api import IRequestFilter, IRequestHandler, RequestDone +from trac.web.auth import LoginModule from trac.wiki.web_ui import WikiModule from trac.util import Markup from trac.util.html import tag @@ -91,3 +92,41 @@ def _format_changeset_link(self, formatter, ns, chgset, label, fullmatch=None): return super(GitHubBrowserWithSVNChangesets, self)._format_changeset_link( formatter, ns, chgset, label, fullmatch ) + + +class PlainLoginComponent(Component): + """ + Enable login through a plain HTML form (no more HTTP basic auth) + """ + implements(IRequestHandler) + + def match_request(self, req): + return req.path_info == '/login' + + def process_request(self, req): + if req.method == 'POST': + return self.do_post(req) + elif req.method == 'GET': + return self.do_get(req) + else: + req.send_response(405) + raise RequestDone + + def do_get(self, req): + # XXX: Importing at the toplevel causes an AppRegistryNotReady as of now + # Probably because we're inside __init__ so when gunicorn tries to import wsgi.py it's + # also loading this module which triggers the error + from django.contrib.auth.forms import AuthenticationForm + return 'plainlogin.html', {'form': AuthenticationForm(), 'next': req.args.get('next', '/')} + + def do_post(self, req): + from django.contrib.auth.forms import AuthenticationForm + form = AuthenticationForm(data=req.args) + if form.is_valid(): + req.environ["REMOTE_USER"] = form.get_user().username + LoginModule(self.compmgr)._do_login(req) + redirect_url = req.args.get('next', '/') + # TODO: validate that the redirect URL is safe to redirect to (same domain at least) + # maybe using django.utils.http.is_safe_url()? + req.redirect(redirect_url) + return 'plainlogin.html', {'form': form, 'next': req.args.get('next', '/')} diff --git a/DjangoPlugin/tracdjangoplugin/djangoauth.py b/DjangoPlugin/tracdjangoplugin/djangoauth.py deleted file mode 100644 index 2af7f91..0000000 --- a/DjangoPlugin/tracdjangoplugin/djangoauth.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -WSGI middleware that authenticates against a Django user database. - -DJANGO_SETTINGS_MODULE should point to a valid Django settings module. - -In addition, the following settings are available: - -- BASIC_AUTH_LOGIN_URL: DjangoAuth will trigger basic authentication on this - URL. Since browsers only propagate auth to resources on the same level or - below, this URL will usually be '/' without a trailing slash. - Defaults to '/login'. -- BASIC_AUTH_MESSAGE: Content of the 401 error page. Defaults to - "Authorization required.". -- BASIC_AUTH_MESSAGE_TYPE: Content type of the message. Defaults to - 'text/plain'. -- BASIC_AUTH_REALM: Authentication realm. Defaults to "Authenticate". -- BASIC_AUTH_REDIRECT_URL: DjangoAuth will redirect to this URL after login if - it isn't empty and to the HTTP Referer otherwise. If provided, it should be - an absolute URL including the domain name. Defaults to ''. - -If the user authenticates successfully, the REMOTE_USER variable is set in the -WSGI environment. - -See http://tools.ietf.org/html/rfc2617#section-2 for details on basic auth. -""" - -from base64 import b64decode - -import django -from django.conf import settings -from django.contrib.auth import authenticate -from django.core.handlers.wsgi import get_path_info -from django.db import close_old_connections -from django.utils import six - - -django.setup() - - -class DjangoAuth: - login_url = getattr(settings, "BASIC_AUTH_LOGIN_URL", "/login") - message = getattr(settings, "BASIC_AUTH_MESSAGE", "Authorization required.") - message_type = getattr(settings, "BASIC_AUTH_MESSAGE_TYPE", "text/plain") - realm = getattr(settings, "BASIC_AUTH_REALM", "Authenticate") - - def __init__(self, application): - self.application = application - - def __call__(self, environ, start_response): - try: - if get_path_info(environ) == self.login_url: - username = self.process_authorization(environ) - if username is None: - start_response( - "401 Unauthorized", - [ - ("Content-Type", self.message_type), - ("WWW-Authenticate", 'Basic realm="%s"' % self.realm), - ], - ) - return [self.message] - finally: - close_old_connections() - - return self.application(environ, start_response) - - @staticmethod - def process_authorization(environ): - # Don't override authentication information set by another component. - remote_user = environ.get("REMOTE_USER") - if remote_user is not None: - return - - authorization = environ.get("HTTP_AUTHORIZATION") - if authorization is None: - return - - if six.PY3: # because fuck you PEP 3333. - authorization = authorization.encode("iso-8859-1").decode("utf-8") - - method, _, credentials = authorization.partition(" ") - if not method.lower() == "basic": - return - - try: - credentials = b64decode(credentials.strip()) - username, _, password = credentials.partition(":") - except Exception: - return - - if authenticate(username=username, password=password) is None: - return - - remote_user = username - - if six.PY3: # because fuck you PEP 3333. - remote_user = remote_user.encode("utf-8").decode("iso-8859-1") - - environ["REMOTE_USER"] = remote_user - - return username diff --git a/DjangoPlugin/tracdjangoplugin/settings.py b/DjangoPlugin/tracdjangoplugin/settings.py index 436d815..83f28ad 100644 --- a/DjangoPlugin/tracdjangoplugin/settings.py +++ b/DjangoPlugin/tracdjangoplugin/settings.py @@ -24,5 +24,3 @@ SECRET_KEY = str(SECRETS["secret_key"]) - -BASIC_AUTH_REALM = "Django's Trac" diff --git a/DjangoPlugin/tracdjangoplugin/wsgi.py b/DjangoPlugin/tracdjangoplugin/wsgi.py index afcfc68..5db4d4e 100644 --- a/DjangoPlugin/tracdjangoplugin/wsgi.py +++ b/DjangoPlugin/tracdjangoplugin/wsgi.py @@ -1,21 +1,17 @@ import os import trac.web.main - application = trac.web.main.dispatch_request +import django +django.setup() + # Massive hack to make Trac fast, otherwise every git call tries to close ulimit -n (1e6) fds # Python 3 would perform better here, but we are still on 2.7 for Trac, so leak fds for now. from tracopt.versioncontrol.git import PyGIT - PyGIT.close_fds = False -from .djangoauth import DjangoAuth - -application = DjangoAuth(application) - trac_dsn = os.getenv("SENTRY_DSN") - if trac_dsn: import sentry_sdk from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware diff --git a/trac-env/conf/trac.ini b/trac-env/conf/trac.ini index 06f47ae..ed17c18 100644 --- a/trac-env/conf/trac.ini +++ b/trac-env/conf/trac.ini @@ -22,6 +22,7 @@ trac.ticket.roadmap.roadmapmodule = disabled trac.versioncontrol.web_ui.browser.browsermodule = disabled trac.versioncontrol.web_ui.changeset.changesetmodule = disabled trac.versioncontrol.web_ui.log.logmodule = disabled +trac.web.auth.loginmodule = disabled; replaced by djangoplugin.PlainLoginComponent trac.wiki.web_ui.wikimodule = disabled tracdjangoplugin.* = enabled tracext.github.githubloginmodule = enabled diff --git a/trac-env/templates/django_theme.html b/trac-env/templates/django_theme.html index 60860c0..3f8fdd0 100644 --- a/trac-env/templates/django_theme.html +++ b/trac-env/templates/django_theme.html @@ -28,8 +28,7 @@ # else -
  • GitHub Login
  • -
  • DjangoProject Login
  • +
  • Login
  • # endif
  • Preferences
  • # if req.perm.has_permission('XML_RPC'): diff --git a/trac-env/templates/plainlogin.html b/trac-env/templates/plainlogin.html new file mode 100644 index 0000000..b0f24f9 --- /dev/null +++ b/trac-env/templates/plainlogin.html @@ -0,0 +1,36 @@ +# extends 'layout.html' + +# block title + Login ${ super() } +# endblock title + +# block content +

    Choose how you want to log in

    + +
    +

    Log in with you GitHub account

    + Log in with GitHub +
    + + + + +# endblock content