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 ``$XDG_CONFIG_HOME`` (`#920`_).
* Added support for custom content types for uploaded files (`#668`_). * Added support for custom content types for uploaded files (`#668`_).
* Added support for ``Set-Cookie``-triggered cookie expiration (`#853`_). * 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) `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 .. _#128: https://github.com/jakubroztocil/httpie/issues/128
.. _#488: https://github.com/jakubroztocil/httpie/issues/488 .. _#488: https://github.com/jakubroztocil/httpie/issues/488
.. _#668: https://github.com/jakubroztocil/httpie/issues/668 .. _#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 .. _#840: https://github.com/jakubroztocil/httpie/issues/840
.. _#853: https://github.com/jakubroztocil/httpie/issues/853 .. _#853: https://github.com/jakubroztocil/httpie/issues/853
.. _#852: https://github.com/jakubroztocil/httpie/issues/852
.. _#870: https://github.com/jakubroztocil/httpie/issues/870 .. _#870: https://github.com/jakubroztocil/httpie/issues/870
.. _#895: https://github.com/jakubroztocil/httpie/issues/895 .. _#895: https://github.com/jakubroztocil/httpie/issues/895
.. _#920: https://github.com/jakubroztocil/httpie/issues/920 .. _#920: https://github.com/jakubroztocil/httpie/issues/920
.. _#925: https://github.com/jakubroztocil/httpie/issues/925 .. _#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 textwrap import dedent
from urllib.parse import urlsplit from urllib.parse import urlsplit
from requests.utils import get_netrc_auth
from httpie.cli.argtypes import AuthCredentials, KeyValueArgType, parse_auth from httpie.cli.argtypes import AuthCredentials, KeyValueArgType, parse_auth
from httpie.cli.constants import ( from httpie.cli.constants import (
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
@ -154,7 +156,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.env.stdout_isatty = False self.env.stdout_isatty = False
def _process_auth(self): def _process_auth(self):
# TODO: refactor # TODO: refactor & simplify this method.
self.args.auth_plugin = None self.args.auth_plugin = None
default_auth_plugin = plugin_manager.get_auth_plugins()[0] default_auth_plugin = plugin_manager.get_auth_plugins()[0]
auth_type_set = self.args.auth_type is not None 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 self.args.auth_type = default_auth_plugin.auth_type
plugin = plugin_manager.get_auth_plugin(self.args.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: if plugin.auth_require and self.args.auth is None:
self.error('--auth required') self.error('--auth required')

View File

@ -35,13 +35,22 @@ class AuthPlugin(BasePlugin):
# Set this to `False` to disable the parsing and error handling. # Set this to `False` to disable the parsing and error handling.
auth_parse = True 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`, # If both `auth_parse` and `prompt_password` are set to `True`,
# and the value of `-a` lacks the password part, # and the value of `-a` lacks the password part,
# then the user will be prompted to type the password in. # then the user will be prompted to type the password in.
prompt_password = True prompt_password = True
# Will be set to the raw value of `-a` (if provided) before # 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 raw_auth = None
def get_auth(self, username=None, password=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 See https://github.com/jakubroztocil/httpie/issues/212
""" """
# noinspection PyTypeChecker
request.headers['Authorization'] = type(self).make_header( request.headers['Authorization'] = type(self).make_header(
self.username, self.password).encode('latin1') self.username, self.password).encode('latin1')
return request return request
@ -36,6 +37,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
class BasicAuthPlugin(BuiltinAuthPlugin): class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth' name = 'Basic HTTP auth'
auth_type = 'basic' auth_type = 'basic'
netrc_parse = True
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
def get_auth(self, username: str, password: str) -> HTTPBasicAuth: def get_auth(self, username: str, password: str) -> HTTPBasicAuth:
@ -43,9 +45,9 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
class DigestAuthPlugin(BuiltinAuthPlugin): class DigestAuthPlugin(BuiltinAuthPlugin):
name = 'Digest HTTP auth' name = 'Digest HTTP auth'
auth_type = 'digest' auth_type = 'digest'
netrc_parse = True
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
def get_auth( def get_auth(

View File

@ -3,6 +3,7 @@ import mock
import pytest import pytest
from httpie.plugins.builtin import HTTPBasicAuth from httpie.plugins.builtin import HTTPBasicAuth
from httpie.status import ExitStatus
from httpie.utils import ExplicitNullAuth from httpie.utils import ExplicitNullAuth
from utils import http, add_auth, HTTP_OK, MockEnvironment from utils import http, add_auth, HTTP_OK, MockEnvironment
import httpie.cli.constants import httpie.cli.constants
@ -15,6 +16,7 @@ def test_basic_auth(httpbin_both):
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.parametrize('argument_name', ['--auth-type', '-A']) @pytest.mark.parametrize('argument_name', ['--auth-type', '-A'])
def test_digest_auth(httpbin_both, argument_name): def test_digest_auth(httpbin_both, argument_name):
r = http(argument_name + '=digest', '--auth=user:password', r = http(argument_name + '=digest', '--auth=user:password',
@ -77,6 +79,8 @@ def test_missing_auth(httpbin):
def test_netrc(httpbin_both): 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: with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password') get_netrc_auth.return_value = ('httpie', 'password')
r = http(httpbin_both + '/basic-auth/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): 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') get_netrc_auth.return_value = ('httpie', 'password')
r = http('--ignore-netrc', httpbin_both + '/basic-auth/httpie/password') r = http('--ignore-netrc', httpbin_both + '/basic-auth/httpie/password')
assert get_netrc_auth.call_count == 0 assert get_netrc_auth.call_count == 0
assert 'HTTP/1.1 401 UNAUTHORIZED' in r 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(): def test_ignore_netrc_together_with_auth():
args = httpie.cli.definition.parser.parse_args( args = httpie.cli.definition.parser.parse_args(
args=['--ignore-netrc', '--auth=username:password', 'example.org'], args=['--ignore-netrc', '--auth=username:password', 'example.org'],
@ -107,10 +103,40 @@ def test_ignore_netrc_together_with_auth():
) )
assert isinstance(args.auth, HTTPBasicAuth) 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)