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

Reply via email to