jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1323814?usp=email )

Change subject: Allow -user to override OAuth authentication
......................................................................

Allow -user to override OAuth authentication

Remember usernames selected by the global -user option. When a site's
OAuth token authenticates as another user, disable OAuth for that site
and retry with the selected user's cookie or password credentials.

Keep OAuth enabled when identities match and preserve configuration
mismatch errors without an explicit override.

Bug: T420084
Change-Id: I9caf1ea43a1a91dccbd500e54f156089ed4bcde1
---
M pywikibot/bot.py
M pywikibot/comms/http.py
M pywikibot/site/_apisite.py
M tests/bot_tests.py
M tests/http_tests.py
M tests/site_tests.py
6 files changed, 179 insertions(+), 6 deletions(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified




diff --git a/pywikibot/bot.py b/pywikibot/bot.py
index 8981cae..4afbfa3 100644
--- a/pywikibot/bot.py
+++ b/pywikibot/bot.py
@@ -282,6 +282,8 @@
 
 #: global args used by tests via pwb wrapper
 global_args: list[str] | None = None
+#: site and username selected with the global ``-user`` option
+_user_override: tuple[str, str, str] | None = None


 def set_interface(module_name: str) -> None:
@@ -780,7 +782,7 @@
         # not the one in pywikibot.bot.
         args = pywikibot.argvu[1:]

-    global global_args
+    global global_args, _user_override
     global_args = args

     # get the name of the module calling this function. This is
@@ -883,6 +885,7 @@
                 non_global_args.append(arg)

     if username:
+        _user_override = (config.family, config.mylang, username)
         config.usernames[config.family][config.mylang] = username

     init_handlers()
diff --git a/pywikibot/comms/http.py b/pywikibot/comms/http.py
index c9e6cf7..bf09359 100644
--- a/pywikibot/comms/http.py
+++ b/pywikibot/comms/http.py
@@ -327,6 +327,8 @@
     headers['user-agent'] = user_agent(site, format_string)

     baseuri = site.base_url(uri, protocol=kwargs.pop('protocol', None))
+    if getattr(site, '_oauth_disabled', False):
+        kwargs.setdefault('auth', None)
     r = fetch(baseuri, headers=headers, **kwargs)
     retry_after = r.headers.get('retry-after', '0')
     # literal of retry_after may int or float (T414197)
@@ -455,6 +457,9 @@
     :type verify: bool or path to certificates
     :keyword callbacks: Methods to call once data is fetched
     :type callbacks: list of callable
+    :keyword auth: Authentication handler. If omitted, authentication is read
+        from :attr:`config.authenticate`; use ``None`` to disable configured
+        authentication.
     """
     # Change user agent depending on fake UA settings.
     # Set header to new UA if needed.
@@ -496,8 +501,11 @@

     charset = kwargs.pop('charset', None)

-    auth = get_authentication(uri)
-    if auth is not None and len(auth) == 4:
+    if 'auth' in kwargs:
+        auth = kwargs.pop('auth')
+    else:
+        auth = get_authentication(uri)
+    if isinstance(auth, tuple) and len(auth) == 4:
         if isinstance(requests_oauthlib, ImportError):
             raise ModuleNotFoundError(f"""{requests_oauthlib}. Install it with

diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index 6185563..c4027b2 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -18,7 +18,7 @@
 from warnings import warn

 import pywikibot
-from pywikibot import login
+from pywikibot import bot, login
 from pywikibot.comms import http
 from pywikibot.data import api
 from pywikibot.exceptions import (
@@ -135,6 +135,7 @@
     ) -> None:
         """Initializer."""
         super().__init__(code, fam, user)
+        self._oauth_disabled = False
         self._globaluserinfo: dict[int | str, Any] = {}
         self._interwikimap = _InterwikiMap(self)
         self._msgcache: dict[str, str] = {}
@@ -339,9 +340,21 @@

     def is_oauth_token_available(self) -> bool:
         """Check whether OAuth token is set for this site."""
+        if getattr(self, '_oauth_disabled', False):
+            return False
         auth_token = http.get_authentication(self.base_url(''))
         return auth_token is not None and len(auth_token) == 4

+    def _is_global_user_override(self) -> bool:
+        """Return whether this site uses the global ``-user`` option."""
+        override = bot._user_override
+        return (
+            override is not None
+            and self.family.name == override[0]
+            and self.code == override[1]
+            and normalize_username(override[2]) == self.username()
+        )
+
     def login(
         self,
         autocreate: bool = False,
@@ -411,6 +424,19 @@
                 raise

         if self.is_oauth_token_available():
+            if (self.userinfo['name'] != self.username()
+                    and self._is_global_user_override()):
+                oauth_user = self.userinfo['name']
+                self._oauth_disabled = True
+                pywikibot.info(
+                    f'OAuth authentication on {self} uses {oauth_user}; '
+                    f'ignoring it because -user:{self.username()} was given.')
+                del self.userinfo
+                self._loginstatus = login.LoginStatus.NOT_ATTEMPTED
+                self.login(autocreate=autocreate, user=user,
+                           cookie_only=cookie_only)
+                return
+
             if self.userinfo['name'] == self.username():
                 error_msg = f'Logging in on {self} via OAuth failed'
             elif self.username() is None:
diff --git a/tests/bot_tests.py b/tests/bot_tests.py
index bc829d5..7de4e39 100755
--- a/tests/bot_tests.py
+++ b/tests/bot_tests.py
@@ -12,10 +12,11 @@
 from contextlib import suppress
 from tempfile import TemporaryDirectory
 from textwrap import dedent
+from unittest.mock import patch

 import pywikibot
 import pywikibot.bot
-from pywikibot import i18n
+from pywikibot import config, i18n
 from tests.aspects import DefaultSiteTestCase, SiteAttributeTestCase, TestCase
 from tests.utils import execute

@@ -78,6 +79,31 @@
         super().setUpClass()


+class TestHandleArgs(TestCase):
+
+    """Test global argument handling."""
+
+    net = False
+
+    @patch('pywikibot.bot.init_handlers')
+    @patch('pywikibot.bot.writeToCommandLogFile')
+    @patch('pywikibot.bot.calledModuleName',
+           return_value='generate_user_files')
+    def test_user_override(self, *unused) -> None:
+        """Test that an explicit username override is retained."""
+        with patch.object(config, 'family', 'wikipedia'), \
+             patch.object(config, 'mylang', 'test'), \
+             patch.object(pywikibot.bot, '_user_override', None), \
+             patch.object(pywikibot, '_sites', {}), \
+             patch.dict(config.usernames['wikipedia'], {}, clear=True):
+            result = pywikibot.handle_args(['-user:Xqt'], do_help=False)
+
+            self.assertIsEmpty(result)
+            self.assertEqual(pywikibot.bot._user_override,
+                             ('wikipedia', 'test', 'Xqt'))
+            self.assertEqual(config.usernames['wikipedia']['test'], 'Xqt')
+
+
 class TestBotTreatExit:

     """Mixin to provide handling for treat and exit."""
diff --git a/tests/http_tests.py b/tests/http_tests.py
index e6db15d..7f8284b 100755
--- a/tests/http_tests.py
+++ b/tests/http_tests.py
@@ -86,6 +86,19 @@
             for url, auth in pairs.items():
                 self.assertEqual(http.get_authentication(url), auth)

+    @patch.object(http.session, 'request')
+    def test_disable_url_authentication(self, request) -> None:
+        """Test configured authentication can be explicitly disabled."""
+        response = requests.Response()
+        response.request = requests.Request(headers={})
+        response.url = 'https://en.wikipedia.beta.wmcloud.org'
+        response._content = b''
+        request.return_value = response
+        http.fetch('https://en.wikipedia.beta.wmcloud.org', auth=None,
+                   default_error_handling=False)
+
+        self.assertIsNone(request.call_args.kwargs['auth'])
+

 class HttpsCertificateTestCase(TestCase):

diff --git a/tests/site_tests.py b/tests/site_tests.py
index 9cb2d17..ed8ef56 100755
--- a/tests/site_tests.py
+++ b/tests/site_tests.py
@@ -15,19 +15,22 @@
 from collections.abc import Iterable, Mapping
 from contextlib import suppress
 from unittest import mock
-from unittest.mock import patch
+from unittest.mock import Mock, patch

 import pywikibot
 from pywikibot import config
+from pywikibot.comms import http
 from pywikibot.exceptions import (
     APIError,
     Error,
     IsNotRedirectPageError,
     NoPageError,
+    NoUsernameError,
     PageInUseError,
     UnknownExtensionError,
     UnknownSiteError,
 )
+from pywikibot.login import LoginStatus
 from tests.aspects import (
     DefaultSiteTestCase,
     DeprecationTestCase,
@@ -39,6 +42,100 @@
 from tests.basepage import BasePageLoadRevisionsCachingTestBase


+class _OAuthLoginSite(pywikibot.site.APISite):
+
+    """APISite stub providing OAuth and cookie identities."""
+
+    def __init__(self, oauth_user: str, cookie_user: str | None) -> None:
+        pywikibot.site.BaseSite.__init__(
+            self, 'test', 'wikipedia', user='Xqt')
+        self._oauth_user = oauth_user
+        self._cookie_user = cookie_user
+        self._oauth_disabled = False
+        self._userinfo = {}
+        self._loginstatus = LoginStatus.NOT_ATTEMPTED
+
+    @property
+    def userinfo(self) -> dict:
+        """Return the identity for the effective authentication method."""
+        username = (self._cookie_user if self._oauth_disabled
+                    else self._oauth_user)
+        if username is None:
+            return {'name': '', 'id': 0, 'anon': ''}
+        return {'name': username, 'id': 1}
+
+    @userinfo.deleter
+    def userinfo(self) -> None:
+        """Ignore attempts to clear the generated userinfo."""
+
+
+class TestOAuthUserOverride(PatchingTestCase):
+
+    """Test OAuth interaction with the global ``-user`` option."""
+
+    net = False
+    oauth_tokens = ('consumer', 'secret', 'access', 'secret')
+
+    def setUp(self) -> None:
+        """Set OAuth and site configuration."""
+        super().setUp()
+        self.patch(config, 'authenticate',
+                   {'*.wikipedia.org': self.oauth_tokens})
+        self.patch(config, 'family', 'wikipedia')
+        self.patch(config, 'mylang', 'test')
+        self.patch(http.cookie_jar, 'load', Mock())
+
+    def test_matching_oauth_user(self) -> None:
+        """Keep OAuth when its identity matches the explicit username."""
+        site = _OAuthLoginSite('Xqt', None)
+        override = ('wikipedia', 'test', 'Xqt')
+        with patch.object(pywikibot.bot, '_user_override', override), \
+             patch.object(pywikibot, 'info') as info:
+            site.login(cookie_only=True)
+
+        self.assertFalse(site._oauth_disabled)
+        self.assertTrue(site.logged_in())
+        info.assert_not_called()
+
+    def test_mismatching_oauth_user_override(self) -> None:
+        """Bypass OAuth when ``-user`` requests another identity."""
+        site = _OAuthLoginSite('Pywikibot-oauth', 'Xqt')
+        override = ('wikipedia', 'test', 'Xqt')
+        with patch.object(pywikibot.bot, '_user_override', override), \
+             patch.object(pywikibot, 'info') as info:
+            site.login(cookie_only=True)
+
+        self.assertTrue(site._oauth_disabled)
+        self.assertTrue(site.logged_in())
+        info.assert_called_once_with(
+            'OAuth authentication on wikipedia:test uses Pywikibot-oauth; '
+            'ignoring it because -user:Xqt was given.')
+
+    def test_mismatching_configured_oauth_user(self) -> None:
+        """Reject an OAuth mismatch without an explicit user override."""
+        site = _OAuthLoginSite('Pywikibot-oauth', 'Xqt')
+        with patch.object(pywikibot.bot, '_user_override', None), \
+             self.assertRaisesRegex(NoUsernameError,
+                                    'via OAuth as Pywikibot-oauth'):
+            site.login(cookie_only=True)
+
+        self.assertFalse(site._oauth_disabled)
+
+    def test_disabled_oauth_not_added_to_request(self) -> None:
+        """Do not add configured OAuth to requests for an override site."""
+        site = _OAuthLoginSite('Pywikibot-oauth', 'Xqt')
+        site._oauth_disabled = True
+        response = Mock(headers={})
+
+        with patch.object(http, 'fetch', return_value=response) as fetch:
+            http.request(site, site.apipath())
+
+        self.assertIsNone(fetch.call_args.kwargs['auth'])
+        self.assertEqual(
+            http.get_authentication('https://en.wikipedia.org'),
+            self.oauth_tokens)
+
+
 class TestSiteObject(DefaultSiteTestCase):

     """Test cases for Site methods."""

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1323814?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings?usp=email

Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I9caf1ea43a1a91dccbd500e54f156089ed4bcde1
Gerrit-Change-Number: 1323814
Gerrit-PatchSet: 6
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to