Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*.pyc
__pycache__
/.cache
/.coverage
/htmlcov
/*.egg-info
__pycache__
.*.sw?
*.pyc
30 changes: 22 additions & 8 deletions jade/centralauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
''' # noqa: E501
import mwapi

from . import about
from . import errors

USER_AGENT = "{} -- {}".format(about.__name__, about.__author_email__)

class CentralAuth:

class CentralAuth(object):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def __init__(self, ca_session):
self.ca_session = ca_session
Expand All @@ -31,8 +34,7 @@ def get_globaluser_info(self, gu_id):
return gui_doc

def get_localuser_info(self, name, context_doc):
session = mwapi.Session(
context_doc['url'], self.ca_session.user_agent)
session = mwapi.Session(context_doc['url'], user_agent = USER_AGENT)
doc = session.get(
action='query', list='users', ususers={name}, usprop={'groups'})
lui_doc = doc['query']['users'][0]
Expand All @@ -45,17 +47,18 @@ def check_user_rights(self, gu_id, context, requirements):
gui_doc = self.get_globaluser_info(gu_id)

if 'locked' in gui_doc:
raise errors.UserLockedError(
"The user account with gu_id={0} is locked".format(gu_id))
raise errors.UserLockedError(gu_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Much better


# Check for local context
if context in gui_doc['merged']:
context_doc = gui_doc['merged'][context]

if 'blocked' in context_doc:
raise errors.UserBlockedError(
"the user account with gu_id={0} is blocked on {1}: {2}"
.format(gu_id, context, context_doc['blocked']))
gu_id,
context,
context_doc['blocked']['expiry'],
context_doc['blocked']['reason'])

lui_doc = self.get_localuser_info(gui_doc['name'], context_doc)
local_groups = lui_doc.get('groups', []) or []
Expand All @@ -66,4 +69,15 @@ def check_user_rights(self, gu_id, context, requirements):

@classmethod
def from_config(cls, config):
return cls(config['centralauth'])
# Extract config.
session_config = config['centralauth'].copy()
if 'user_agent' not in session_config:
session_config['user_agent'] = USER_AGENT
# Pull host out as it's not a keyword arg.
host = session_config['host']
del(session_config['host'])

# Create session based on config.
ca_session = mwapi.Session(host, **session_config)

return cls(ca_session)
41 changes: 41 additions & 0 deletions jade/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ class UserPermissionError(RequestError):
HTTP_CODE = 403


class GlobalUserExistenceError(UserPermissionError):
"This user is globally missing."
SUBTYPE = "user_missing_global"
HTTP_CODE = 403

def __init__(self, gu_id):
super().__init__()
self.gu_id = gu_id

def format_detail(self):
return {'gu_id': self.gu_id}


class LocalUserExistenceError(UserPermissionError):
"This user is locally missing."
SUBTYPE = "user_missing_local"
HTTP_CODE = 403

def __init__(self, name, context):
super().__init__()
self.name = name
self.context = context

def format_detail(self):
return {'name': self.name,
'context': self.context}


class UserBlockedError(UserPermissionError):
"This action cannot be performed because the user is blocked."
SUBTYPE = "user_blocked"
Expand All @@ -110,6 +138,19 @@ def format_detail(self):
'reason': self.reason}


class UserLockedError(UserPermissionError):
"This action cannot be performed because the user is blocked."
SUBTYPE = "user_locked"
HTTP_CODE = 403

def __init__(self, gu_id):
super().__init__()
self.gu_id = gu_id

def format_detail(self):
return {'gu_id': self.gu_id}


class UserRightsError(UserPermissionError):
"This action cannot be performed because the user lacks necessary rights."
SUBTYPE = "user_rights"
Expand Down
Empty file added jade/tests/__init__.py
Empty file.
249 changes: 249 additions & 0 deletions jade/tests/test_centralauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import mwapi
import pytest

from jade.centralauth import CentralAuth
from jade import errors
from . import util

test_config = {
'centralauth': {
# Global info host can be arbitrary?
'host': 'https://www.mediawiki.org',
},
}

# TODO: Made this up cos I haven't found a missing user yet.
missing_gui_response = {
'batchcomplete': '',
'query': {
'globaluserinfo': {
'missing': '',
}
}
}

enwiki_blocked_gui_response = {
'batchcomplete': '',
'query': {
'globaluserinfo': {
'home': 'enwiki',
'id': 19729909,
'registration': '2013-04-14T06:48:25Z',
'name': 'FUCKYOU',
'groups': [],
'rights': [],
'merged': [
{
'wiki': 'enwiki',
'url': 'https://en.wikipedia.org',
'timestamp': '2013-04-14T06:48:25Z',
'method': 'primary',
'editcount': 0,
'registration': '2006-03-26T17:01:06Z',
'blocked': {
'expiry': 'infinity',
'reason': '{{username}}'
},
},
]
}
}
}

locked_gui_response = {
'batchcomplete': '',
'query': {
'globaluserinfo': {
'home': 'metawiki',
'id': 33085348,
'registration': '2014-08-17T23:22:31Z',
'name': 'Jalexander',
'locked': '',
'groups': [],
'rights': [],
'merged': [
{
'wiki': 'commonswiki',
'url': 'https://commons.wikimedia.org',
'timestamp': '2017-12-12T19:44:56Z',
'method': 'login',
'editcount': 0,
'registration': '2017-12-12T19:44:56Z',
},
{
'wiki': 'loginwiki',
'url': 'https://login.wikimedia.org',
'timestamp': '2014-08-17T23:22:33Z',
'method': 'login',
'editcount': 0,
'registration': '2014-08-17T23:22:33Z',
},
{
'wiki': 'metawiki',
'url': 'https://meta.wikimedia.org',
'timestamp': '2014-08-17T23:22:31Z',
'method': 'new',
'editcount': 0,
'registration': '2014-08-17T23:22:31Z',
},
{
'wiki': 'nowikimedia',
'url': 'https://no.wikimedia.org',
'timestamp': '2017-12-13T09:53:07Z',
'method': 'login',
'editcount': 0,
'registration': '2017-12-13T09:53:07Z',
}
]
}
}
}

autopatrolled_metawiki_gui_response = {
"batchcomplete": "",
"query": {
"globaluserinfo": {
"home": "enwiki",
"id": 15622560,
"registration": "2012-12-21T22:37:22Z",
"name": "Awight (WMF)",
"groups": [
"oathauth-tester",
"wmf-researcher"
],
"merged": [
{
"wiki": "metawiki",
"url": "https://meta.wikimedia.org",
"timestamp": "2012-12-21T23:59:21Z",
"method": "login",
"editcount": 222,
"registration": "2012-12-21T23:59:21Z",
"groups": [
"autopatrolled"
]
},
{
"wiki": "ptwiki",
"url": "https://pt.wikipedia.org",
"timestamp": "2013-04-09T01:26:35Z",
"method": "login",
"editcount": 0,
"registration": "2013-04-09T01:26:35Z"
},
],
"unattached": []
}
}
}

autopatrolled_metawiki_lui_response = {
'batchcomplete': '',
'query': {
'users': [{
'userid': 2043420,
'name': 'Awight (WMF)',
'groups': ['autopatrolled', '*', 'user', 'autoconfirmed']
}]
}
}

no_autopatrolled_metawiki_lui_response = {
'batchcomplete': '',
'query': {
'users': [{
'userid': 2043420,
'name': 'Awight (WMF)',
'groups': ['*', 'user', 'autoconfirmed']
}]
}
}


def test_from_config():
ca = CentralAuth.from_config(test_config)
assert isinstance(ca, CentralAuth)


# TODO: We can't test this using the current mocking strategy, because the
# error comes from within .get
# def test_get_globaluser_info_invalid_user_id():
# ca = CentralAuth.from_config(test_config)
# with pytest.raises(mwapi.errors.APIError):
# ca.get_globaluser_info(0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this TODO.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out, requesting non-existent users actually causes an APIError in mwapi, rather than the "missing" sad path. We probably need to handle this, and calling functions will be expecting differents types of jade.errors.UserPermissionError. We should consider wrapping the APIError... Also, the TODO is because I hadn't decided which code we should be checking. Now I think we should be mocking mwapi.Session.get to just throw an error, but first let's decide how the CentralAuth class will handle it.



def test_get_globaluser_info_missing_user():
ca = CentralAuth.from_config(test_config)

with util.mock_mwapi_get(missing_gui_response):
with pytest.raises(errors.GlobalUserExistenceError) as exc_info:
gui_doc = ca.get_globaluser_info(12345)

assert exc_info.value.gu_id == 12345


def test_get_globaluser_info():
ca = CentralAuth.from_config(test_config)

with util.mock_mwapi_get(enwiki_blocked_gui_response):
gui_doc = ca.get_globaluser_info(19729909)

assert gui_doc['home'] == 'enwiki'
assert gui_doc['name'] == 'FUCKYOU'


def test_check_user_rights_blocked_in_context():
ca = CentralAuth.from_config(test_config)
with util.mock_mwapi_get(enwiki_blocked_gui_response):
with pytest.raises(errors.UserBlockedError) as exc_info:
ca.check_user_rights(19729909, 'enwiki', ['autopatrolled'])

assert exc_info.value.gu_id == 19729909
assert exc_info.value.context == 'enwiki'
assert exc_info.value.expiry == 'infinity'
assert exc_info.value.reason == '{{username}}'


def test_check_user_rights_blocked_other_context():
ca = CentralAuth.from_config(test_config)
with util.mock_mwapi_get(enwiki_blocked_gui_response):
ca.check_user_rights(19729909, 'frwiki', ['autopatrolled'])

assert True


def test_check_user_rights_locked():
ca = CentralAuth.from_config(test_config)
#with mock.patch('mwapi.session.get') as mock_mwapi:
# mock_mwapi.side_effect = [
# enwiki_blocked_gui_response,
# enwiki_blocked_lui_response,
# ]
with util.mock_mwapi_get(locked_gui_response):
with pytest.raises(errors.UserLockedError) as exc_info:
ca.check_user_rights(33085348, 'enwiki', ['autopatrolled'])

assert exc_info.value.gu_id == 33085348


def test_check_user_rights_has_autopatrolled():
ca = CentralAuth.from_config(test_config)
with util.mock_mwapi_get([autopatrolled_metawiki_gui_response,
autopatrolled_metawiki_lui_response]):
ca.check_user_rights(15622560, 'metawiki', ['autopatrolled'])

assert True


def test_check_user_rights_lacks_autopatrolled():
ca = CentralAuth.from_config(test_config)
with util.mock_mwapi_get([autopatrolled_metawiki_gui_response,
no_autopatrolled_metawiki_lui_response]):
with pytest.raises(errors.UserRightsError) as exc_info:
ca.check_user_rights(15622560, 'metawiki', ['autopatrolled'])

assert exc_info.value.gu_id == 15622560
assert exc_info.value.context == 'metawiki'
assert exc_info.value.required == ['autopatrolled']
assert exc_info.value.user_groups == ['*', 'user', 'autoconfirmed']
Loading