BryanDavis has uploaded a new change for review. https://gerrit.wikimedia.org/r/316025
Change subject: Validate new usernames with action=query&list=users&usprop=cancreate ...................................................................... Validate new usernames with action=query&list=users&usprop=cancreate Call the Action API on Wikitech to validate that a desired username is not blacklisted via Titleblacklist or other means. This is much easier than recreating all of the blacklisting logic in Striker itself and trying to somehow keep it in sync with the Wikitech and Wikimedia global rules. This patch introduces a new dependency on the mwclient python library which is used to talk to the Action API. Full production deployment will require an associated update to the labs/striker/wheels.git repository. Bug: T147024 Change-Id: Id79568880efe84bbd12c9cc130cdc5fc9c20541d --- M requirements.txt A striker/mediawiki.py M striker/register/forms.py M striker/register/utils.py M striker/register/views.py 5 files changed, 157 insertions(+), 8 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/labs/striker refs/changes/25/316025/1 diff --git a/requirements.txt b/requirements.txt index c17b1f4..e786a18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ django-parsley>=0.6 # BSD django-ratelimit-backend>=1.0 # BSD idna>=2.1 # BSD +mwclient>=0.8.1 # MIT mwoauth>=0.2.7 # MIT mysqlclient>=1.3.7 # GPLv2 oauthlib>=1.1.2 # BSD diff --git a/striker/mediawiki.py b/striker/mediawiki.py new file mode 100644 index 0000000..2cd3992 --- /dev/null +++ b/striker/mediawiki.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Wikimedia Foundation and contributors. +# All Rights Reserved. +# +# This file is part of Striker. +# +# Striker is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Striker is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Striker. If not, see <http://www.gnu.org/licenses/>. + +import logging +import urllib.parse + +import mwclient + +from django.conf import settings + + +logger = logging.getLogger(__name__) + + +class Client(object): + """MediaWiki client""" + _default_instance = None + + @classmethod + def default_client(cls): + """Get a MediaWiki client using the default credentials.""" + if cls._default_instance is None: + logger.debug('Creating default instance') + cls._default_instance = cls(settings.WIKITECH_URL) + return cls._default_instance + + def __init__(self, url): + self.url = url + self.site = self._site_for_url(url) + self.site.force_login = False + + @classmethod + def _site_for_url(cls, url): + parts = urllib.parse.urlparse(url) + host = parts.netloc + if parts.scheme != 'https': + host = (parts.scheme, parts.netloc) + return mwclient.Site(host, clients_useragent='Striker') + + def query_users_cancreate(self, *users): + """Check to see if the given usernames could be created or not. + + Note: if this Client is authenticated to the target wiki, the result + that you get from this request may or may not be the same result that + an anonymous user would get. + """ + result = self.site.api( + 'query', formatversion=2, + list='users', + usprop='cancreate', ususers='|'.join(users), + ) + + logger.debug(result) + # Example result: + # {'query': {'users': [{'missing': True, 'name': 'Puppet', + # 'cancreate': False, 'cancreateerror': [{'message': + # 'titleblacklist-forbidden-new-account', 'params': [' + # ^(User:)?puppet$ <newaccountonly>', 'Puppet'], 'type': 'error'}]}], + # 'userinfo': {'anon': True, 'messages': False, 'name': + # '137.164.12.107', 'id': 0}}, 'batchcomplete': True} + # TODO: error handling + return result['query']['users'] + + def get_message(self, message, *params, lang='en'): + result = self.site.api( + 'query', formatversion=2, + meta='allmessages', + ammessages=message, amargs='|'.join(params), amlang=lang, + ) + # TODO: error handling + return result['query']['allmessages'][0]['content'] diff --git a/striker/register/forms.py b/striker/register/forms.py index 09d8aaf..2754c09 100644 --- a/striker/register/forms.py +++ b/striker/register/forms.py @@ -60,14 +60,20 @@ def clean_username(self): """Validate that username is available.""" # Make sure that username is capitalized like MW's Title would do. - # TODO: Totally not as fancy as secureAndSplit() and friends. Do we - # need to figure out how to actually do all of that? + # If we get to the check_username_create() call below we will fetch an + # actually sanatized username from MediaWiki. username = self.cleaned_data['username'].strip() username = username[0].upper() + username[1:] if not utils.username_available(username): raise forms.ValidationError(self.IN_USE) - # TODO: check that it isn't banned by some abusefilter type rule - return username + + # Check that it isn't banned by some abusefilter type rule + user = utils.check_username_create(username) + if user['ok'] is False: + raise forms.ValidationError(user['error']) + + # Return the canonicalized username from out MW api request + return user['name'] @parsleyfy @@ -107,7 +113,12 @@ shellname = self.cleaned_data['shellname'] if not utils.shellname_available(shellname): raise forms.ValidationError(self.IN_USE) - # TODO: check that it isn't banned by some abusefilter type rule + + # Check that it isn't banned by some abusefilter type rule + user = utils.check_username_create(shellname) + if user['ok'] is False: + raise forms.ValidationError(user['error']) + return shellname diff --git a/striker/register/utils.py b/striker/register/utils.py index 3e9ece9..3f2b21c 100644 --- a/striker/register/utils.py +++ b/striker/register/utils.py @@ -20,11 +20,16 @@ import logging -from striker.tools.models import Maintainer +from django.utils import translation +from django.utils.translation import ugettext_lazy as _ + +from striker import mediawiki from striker.labsauth.models import LabsUser +from striker.tools.models import Maintainer logger = logging.getLogger(__name__) +mwapi = mediawiki.Client.default_client() def sul_available(name): @@ -52,3 +57,40 @@ return True else: return False + + +def check_username_create(name): + """Check to see if a given name would be allowed as a username. + + Returns True if the username would be allowed. Returns either False or a + reason specifier if the username is not allowed. + Returns a dict with these keys: + - ok : Can a new user be created with this name (True/False) + - name : Canonicalized version of the given name + - error : Error message if ok is False; None otherwise + """ + user = mwapi.query_users_cancreate(name)[0] + # Example response: + # [{'missing': True, 'name': 'Puppet', + # 'cancreate': False, 'cancreateerror': [{'message': + # 'titleblacklist-forbidden-new-account', 'params': [' + # ^(User:)?puppet$ <newaccountonly>', 'Puppet'], 'type': 'error'}]}] + ret = { + 'ok': False, + 'name': user['name'], + 'error': None, + } + if user['missing'] and user['cancreate']: + ret['ok'] = True + elif 'userid' in user: + ret['error'] = _('%(name)s is already in use.') % ret + elif 'cancreateerror' in user: + try: + ret['error'] = mwapi.get_message( + user['cancreateerror'][0]['message'], + *user['cancreateerror'][0]['params'], + lang=translation.get_language().split('-')[0]) + except Exception: + logger.exception('Failed to get expanded message for %s', user) + ret['error'] = user['cancreateerror'][0]['message'] + return ret diff --git a/striker/register/views.py b/striker/register/views.py index aff4842..09a930b 100644 --- a/striker/register/views.py +++ b/striker/register/views.py @@ -29,6 +29,7 @@ from django.http import JsonResponse from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.cache import never_cache from formtools.wizard.views import NamedUrlSessionWizardView @@ -75,28 +76,34 @@ urlresolvers.reverse('register:wizard', kwargs={'step': 'ldap'})) +@never_cache def username_available(req, name): """JSON callback for parsley validation of username. Kind of gross, but it returns a 406 status code when the name is not - available. This is to work with the limit choice of default response + available. This is to work with the limited choice of default response validators in parsley. """ available = utils.username_available(name) + if available: + available = utils.check_username_create(name)['ok'] status = 200 if available else 406 return JsonResponse({ 'available': available, }, status=status) +@never_cache def shellname_available(req, name): """JSON callback for parsley validation of shell username. Kind of gross, but it returns a 406 status code when the name is not - available. This is to work with the limit choice of default response + available. This is to work with the limited choice of default response validators in parsley. """ available = utils.shellname_available(name) + if available: + available = utils.check_username_create(name)['ok'] status = 200 if available else 406 return JsonResponse({ 'available': available, -- To view, visit https://gerrit.wikimedia.org/r/316025 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Id79568880efe84bbd12c9cc130cdc5fc9c20541d Gerrit-PatchSet: 1 Gerrit-Project: labs/striker Gerrit-Branch: master Gerrit-Owner: BryanDavis <bda...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits