Added netrc support for auth plugins.

Enabled for --auth-type=basic and digest, 3rd parties may opt in.

This closes #718, closes #719, closes #852, and also closes #934
This commit is contained in:
Jakub Roztocil 2020-06-16 11:05:00 +02:00
parent c240162cab
commit b86598886e
5 changed files with 76 additions and 18 deletions

View File

@ -15,6 +15,8 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
* Added support for ``$XDG_CONFIG_HOME`` (`#920`_).
* Added support for custom content types for uploaded files (`#668`_).
* Added support for ``Set-Cookie``-triggered cookie expiration (`#853`_).
* Added ``netrc`` support for auth plugins.
Enabled for ``--auth-type=basic`` and ``digest``, 3rd parties may opt in (`#718`_, `#719`_, `#852`_, `#934`_).
`2.1.0`_ (2020-04-18)
@ -439,9 +441,13 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
.. _#128: https://github.com/jakubroztocil/httpie/issues/128
.. _#488: https://github.com/jakubroztocil/httpie/issues/488
.. _#668: https://github.com/jakubroztocil/httpie/issues/668
.. _#718: https://github.com/jakubroztocil/httpie/issues/718
.. _#719: https://github.com/jakubroztocil/httpie/issues/719
.. _#840: https://github.com/jakubroztocil/httpie/issues/840
.. _#853: https://github.com/jakubroztocil/httpie/issues/853
.. _#852: https://github.com/jakubroztocil/httpie/issues/852
.. _#870: https://github.com/jakubroztocil/httpie/issues/870
.. _#895: https://github.com/jakubroztocil/httpie/issues/895
.. _#920: https://github.com/jakubroztocil/httpie/issues/920
.. _#925: https://github.com/jakubroztocil/httpie/issues/925
.. _#934: https://github.com/jakubroztocil/httpie/issues/934

View File

@ -7,6 +7,8 @@ from argparse import RawDescriptionHelpFormatter
from textwrap import dedent
from urllib.parse import urlsplit
from requests.utils import get_netrc_auth
from httpie.cli.argtypes import AuthCredentials, KeyValueArgType, parse_auth
from httpie.cli.constants import (
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
@ -154,7 +156,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.env.stdout_isatty = False
def _process_auth(self):
# TODO: refactor
# TODO: refactor & simplify this method.
self.args.auth_plugin = None
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
auth_type_set = self.args.auth_type is not None
@ -177,6 +179,19 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.args.auth_type = default_auth_plugin.auth_type
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
if (not self.args.ignore_netrc
and self.args.auth is None
and plugin.netrc_parse):
# Only host needed, so its OK URL not finalized.
netrc_credentials = get_netrc_auth(self.args.url)
if netrc_credentials:
self.args.auth = AuthCredentials(
key=netrc_credentials[0],
value=netrc_credentials[1],
sep=SEPARATOR_CREDENTIALS,
orig=SEPARATOR_CREDENTIALS.join(netrc_credentials)
)
if plugin.auth_require and self.args.auth is None:
self.error('--auth required')

View File

@ -35,13 +35,22 @@ class AuthPlugin(BasePlugin):
# Set this to `False` to disable the parsing and error handling.
auth_parse = True
# Set to `True` to make it possible for this auth
# plugin to acquire credentials from the users netrc file(s).
# It is used as a fallback when the credentials are not provided explicitly
# through `--auth, -a`. Enabling this will allow skipping `--auth, -a`
# even when `auth_require` is set `True` (provided that netrc provides
# credential for a given host).
netrc_parse = False
# If both `auth_parse` and `prompt_password` are set to `True`,
# and the value of `-a` lacks the password part,
# then the user will be prompted to type the password in.
prompt_password = True
# Will be set to the raw value of `-a` (if provided) before
# `get_auth()` gets called.
# `get_auth()` gets called. If the credentials came from a netrc file,
# then this is `None`.
raw_auth = None
def get_auth(self, username=None, password=None):

View File

@ -22,6 +22,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
See https://github.com/jakubroztocil/httpie/issues/212
"""
# noinspection PyTypeChecker
request.headers['Authorization'] = type(self).make_header(
self.username, self.password).encode('latin1')
return request
@ -36,6 +37,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth'
auth_type = 'basic'
netrc_parse = True
# noinspection PyMethodOverriding
def get_auth(self, username: str, password: str) -> HTTPBasicAuth:
@ -43,9 +45,9 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
class DigestAuthPlugin(BuiltinAuthPlugin):
name = 'Digest HTTP auth'
auth_type = 'digest'
netrc_parse = True
# noinspection PyMethodOverriding
def get_auth(

View File

@ -3,6 +3,7 @@ import mock
import pytest
from httpie.plugins.builtin import HTTPBasicAuth
from httpie.status import ExitStatus
from httpie.utils import ExplicitNullAuth
from utils import http, add_auth, HTTP_OK, MockEnvironment
import httpie.cli.constants
@ -15,6 +16,7 @@ def test_basic_auth(httpbin_both):
assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.parametrize('argument_name', ['--auth-type', '-A'])
def test_digest_auth(httpbin_both, argument_name):
r = http(argument_name + '=digest', '--auth=user:password',
@ -77,6 +79,8 @@ def test_missing_auth(httpbin):
def test_netrc(httpbin_both):
# This one gets handled by requests (no --auth, --auth-type present),
# thats why we patch inside `requests.sessions`.
with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password')
r = http(httpbin_both + '/basic-auth/httpie/password')
@ -85,21 +89,13 @@ def test_netrc(httpbin_both):
def test_ignore_netrc(httpbin_both):
with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
with mock.patch('httpie.cli.argparser.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password')
r = http('--ignore-netrc', httpbin_both + '/basic-auth/httpie/password')
assert get_netrc_auth.call_count == 0
assert 'HTTP/1.1 401 UNAUTHORIZED' in r
def test_ignore_netrc_null_auth():
args = httpie.cli.definition.parser.parse_args(
args=['--ignore-netrc', 'example.org'],
env=MockEnvironment(),
)
assert isinstance(args.auth, ExplicitNullAuth)
def test_ignore_netrc_together_with_auth():
args = httpie.cli.definition.parser.parse_args(
args=['--ignore-netrc', '--auth=username:password', 'example.org'],
@ -107,10 +103,40 @@ def test_ignore_netrc_together_with_auth():
)
assert isinstance(args.auth, HTTPBasicAuth)
def test_honor_netrc_authtype(httpbin_both):
with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password')
r = http('--auth-type=basic','GET', httpbin_both + '/basic-auth/httpie/password')
assert get_netrc_auth.call_count == 1
assert HTTP_OK in r
def test_ignore_netrc_with_auth_type_resulting_in_missing_auth(httpbin):
with mock.patch('httpie.cli.argparser.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password')
r = http(
'--ignore-netrc',
'--auth-type=basic',
httpbin + '/basic-auth/httpie/password',
tolerate_error_exit_status=True,
)
assert get_netrc_auth.call_count == 0
assert r.exit_status == ExitStatus.ERROR
assert '--auth required' in r.stderr
@pytest.mark.parametrize(
argnames=['auth_type', 'endpoint'],
argvalues=[
('basic', '/basic-auth/httpie/password'),
('digest', '/digest-auth/auth/httpie/password'),
],
)
def test_auth_plugin_netrc_parse(auth_type, endpoint, httpbin):
# Test
with mock.patch('httpie.cli.argparser.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password')
r = http('--auth-type', auth_type, httpbin + endpoint)
assert get_netrc_auth.call_count == 1
assert HTTP_OK in r
def test_ignore_netrc_null_auth():
args = httpie.cli.definition.parser.parse_args(
args=['--ignore-netrc', 'example.org'],
env=MockEnvironment(),
)
assert isinstance(args.auth, ExplicitNullAuth)