Removed passlib to plugin, no-auth defaults, integration tests

This commit is contained in:
Matthew Planchard 2018-08-04 21:22:14 -05:00
parent 84158ab881
commit 824c2dd24f
5 changed files with 84 additions and 90 deletions

@ -148,6 +148,10 @@ class _PypiserverParser(ArgumentParser):
parsed = super(_PypiserverParser, self).parse_args( parsed = super(_PypiserverParser, self).parse_args(
args=args, namespace=namespace args=args, namespace=namespace
) )
if not hasattr(parsed, 'authenticate'):
# Ensure a useful value is present even when no auth
# plugins are installed.
parsed.authenticate = _Defaults.authenticate
for attr, parser in self.extra_parsers.items(): for attr, parser in self.extra_parsers.items():
if hasattr(parsed, attr): if hasattr(parsed, attr):
setattr(parsed, attr, parser(getattr(parsed, attr))) setattr(parsed, attr, parser(getattr(parsed, attr)))
@ -410,24 +414,33 @@ class Config(object):
:param ArgumentParser parser: an ArgumentParser instance :param ArgumentParser parser: an ArgumentParser instance
""" """
auth_plugins_available = bool(
set(self._plugins['authenticators']).difference(
set(('no-auth',))
)
)
security = parser.add_argument_group( security = parser.add_argument_group(
title='Security', title='Security',
description='Configure pypiserver access controls' description='Configure pypiserver access controls'
) )
# TODO: pull some of this long stuff out into an epilog if auth_plugins_available or self.parser_type == 'pypi-server':
# Do not bother to show authentication arguments when no
# non-dummy auth plugins are installed.
security.add_argument( security.add_argument(
'-a', '--authenticate', '-a', '--authenticate',
default=environ.get( default=environ.get(
'PYPISERVER_AUTHENTICATE', 'PYPISERVER_AUTHENTICATE',
_Defaults.authenticate, _Defaults.authenticate,
), ),
# TODO: pull some of this long stuff out into an epilog
help=dedent('''\ help=dedent('''\
comma-separated list of (case-insensitive) actions to comma-separated list of (case-insensitive) actions to
authenticate. Use "." for no authentication. Requires the authenticate. Use "." for no authentication. Requires the
password (-P option) to be set. For example to password-protect password (-P option) to be set. For example to
package downloads (in addition to uploads), while leaving password-protect package downloads (in addition to
listings public, use: `-P foo/htpasswd.txt` -a update,download uploads), while leaving listings public, use:
To drop all authentications, use: `-P . -a `. `-P foo/htpasswd.txt -a update,download`.
To drop all authentications, use: `-P . -a .`.
Note that when uploads are not protected, the `register` Note that when uploads are not protected, the `register`
command is not necessary, but `~/.pypirc` still requires command is not necessary, but `~/.pypirc` still requires
username and password fields, even if bogus. By default, username and password fields, even if bogus. By default,
@ -435,6 +448,8 @@ class Config(object):
''') ''')
) )
if self.parser_type == 'pypi-server': if self.parser_type == 'pypi-server':
# This argument is created by the `pypiserver-passlib` plugin
# for pypiserver>=2.0
security.add_argument( security.add_argument(
'-P', '--passwords', '-P', '--passwords',
dest='password_file', dest='password_file',

@ -1,44 +0,0 @@
"""Authentication based on an htpasswd file."""
from os import environ
from textwrap import dedent
from passlib.apache import HtpasswdFile
from .interface import AuthenticatorInterface
class HtpasswdAuthenticator(AuthenticatorInterface):
"""Authenticate using passlib and an htpasswd file."""
plugin_name = 'Htpasswd Authenticator'
plugin_help = 'Authenticate using an Apache htpasswd file'
def __init__(self, config):
"""Instantiate the authenticator."""
self.config = config
@classmethod
def update_parser(cls, parser):
"""Add htpasswd arguments to the config parser.
:param argparse.ArgumentParser parser: the config parser
"""
parser.add_argument(
'-P', '--passwords',
dest='password_file',
default=environ.get('PYPISERVER_PASSWORD_FILE'),
help=dedent('''\
use apache htpasswd file PASSWORD_FILE to set usernames &
passwords when authenticating certain actions (see -a option).
''')
)
def authenticate(self, request):
"""Authenticate the provided request."""
if (self.config.password_file is None or
self.config.password_file == '.'):
return True
pwd_file = HtpasswdFile(self.config.password_file)
pwd_file.load_if_changed()
return pwd_file.check_password(*request.auth)

@ -42,12 +42,12 @@ setup(
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
setup_requires=setup_requires, setup_requires=setup_requires,
extras_require={ extras_require={
'passlib': ['passlib>=1.6'], 'passlib': ['pypiserver-passlib'],
'cache': ['watchdog'] 'cache': ['watchdog']
}, },
tests_require=tests_require, tests_require=tests_require,
url="https://github.com/pypiserver/pypiserver", url="https://github.com/pypiserver/pypiserver",
maintainer=("Kostis Anagnostopoulos <ankostis@gmail.com>" maintainer=("Kostis Anagnostopoulos <ankostis@gmail.com> "
"Matthew Planchard <mplanchard@gmail.com>"), "Matthew Planchard <mplanchard@gmail.com>"),
maintainer_email="ankostis@gmail.com", maintainer_email="ankostis@gmail.com",
classifiers=[ classifiers=[
@ -70,7 +70,8 @@ setup(
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Build Tools",
"Topic :: System :: Software Distribution"], "Topic :: System :: Software Distribution"
],
zip_safe=True, zip_safe=True,
entry_points={ entry_points={
'paste.app_factory': ['main=pypiserver.paste:paste_app_factory'], 'paste.app_factory': ['main=pypiserver.paste:paste_app_factory'],
@ -79,9 +80,6 @@ setup(
'pypiserver=pypiserver.__main__:main', 'pypiserver=pypiserver.__main__:main',
], ],
'pypiserver.authenticators': [ 'pypiserver.authenticators': [
'htpasswd = '
'pypiserver.plugins.authenticators.htpasswd:HtpasswdAuthenticator '
'[passlib]',
'no-auth = ' 'no-auth = '
'pypiserver.plugins.authenticators.no_auth:NoAuthAuthenticator' 'pypiserver.plugins.authenticators.no_auth:NoAuthAuthenticator'
] ]

@ -2,7 +2,7 @@
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from os import chdir, getcwd, environ, path, remove from os import chdir, getcwd, environ, listdir, path, remove
from shutil import copy2, rmtree from shutil import copy2, rmtree
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from tempfile import mkdtemp from tempfile import mkdtemp
@ -52,35 +52,52 @@ def activate_venv(venv_dir):
environ['VIRTUAL_ENV'] = start_venv environ['VIRTUAL_ENV'] = start_venv
def pypiserver_cmd(venv_dir, root, *args): def pypiserver_cmd(root, *args):
"""Yield a command to run pypiserver. """Yield a command to run pypiserver.
:param str exc: the path to the python executable to use in :param str exc: the path to the python executable to use in
running pypiserver. running pypiserver.
:param args: extra arguments for ``pypiserver run`` :param args: extra arguments for ``pypiserver run``
""" """
yield '{}/bin/pypiserver'.format(venv_dir) # yield '{}/bin/pypiserver'.format(venv_dir)
yield 'pypiserver'
yield 'run' yield 'run'
yield root yield root
for arg in args: for arg in args:
yield arg yield arg
def pip_cmd(venv_dir, *args): def pip_cmd(*args):
"""Yield a command to run pip. """Yield a command to run pip.
:param str bindir: the path to the bin directory where the pip
command can be found.
:param args: extra arguments for ``pip`` :param args: extra arguments for ``pip``
""" """
yield '{}/bin/pip'.format(venv_dir) yield 'pip'
for arg in args: for arg in args:
yield arg yield arg
if 'install' in args or 'download' in args: if any(i in args for i in ('install', 'download', 'search')):
yield '--index-url' yield '-i'
yield 'http://localhost:8080' yield 'http://localhost:8080'
def twine_cmd(*args):
"""Yield a command to run twine.
:param args: arguments for `twine`
"""
yield 'twine'
for arg in args:
yield arg
for part in ('--repository-url', 'http://localhost:8080'):
yield part
if '-u' not in args:
for part in ('-u', 'username'):
yield part
if '-p' not in args:
for part in ('-p', 'password'):
yield part
def run(args, raise_on_err=True, capture=False, **kwargs): def run(args, raise_on_err=True, capture=False, **kwargs):
"""Straightforward implementation to run subprocesses. """Straightforward implementation to run subprocesses.
@ -112,7 +129,6 @@ def venv():
venv_dir, venv_dir,
)) ))
with activate_venv(venv_dir): with activate_venv(venv_dir):
import ipdb; ipdb.set_trace()
run( run(
( (
'python', 'python',
@ -161,9 +177,7 @@ class TestNoAuth:
"""Run pypiserver with no auth.""" """Run pypiserver with no auth."""
pkg_root = mkdtemp() pkg_root = mkdtemp()
with activate_venv(venv): with activate_venv(venv):
proc = Popen(pypiserver_cmd( proc = Popen(pypiserver_cmd(pkg_root), env=environ)
venv, pkg_root, '--auth-backend', 'no-auth'
), env=environ)
yield pkg_root yield pkg_root
proc.kill() proc.kill()
rmtree(pkg_root) rmtree(pkg_root)
@ -182,10 +196,21 @@ class TestNoAuth:
yield yield
remove(path.join(pkg_root, SIMPLE_DEV_PKG)) remove(path.join(pkg_root, SIMPLE_DEV_PKG))
@pytest.mark.usefixtures('venv_active') @pytest.mark.usefixtures('venv_active', 'simple_pkg')
def test_install(self, venv, simple_pkg): def test_install(self):
"""Test pulling a package with pip from the repo.""" """Test pulling a package with pip from the repo."""
run(('pip', 'install', 'simple_pkg')) run(pip_cmd('install', 'simple_pkg'))
assert 'simple-pkg' in run( assert 'simple-pkg' in run(pip_cmd('freeze'), capture=True)
('pip', 'freeze'), capture=True, env=environ
) @pytest.mark.usefixtures('venv_active')
def test_upload(self, pkg_root):
"""Test putting a package into the rpeo."""
assert SIMPLE_PKG not in listdir(pkg_root)
run(twine_cmd('upload', SIMPLE_PKG_PATH))
assert SIMPLE_PKG in listdir(pkg_root)
@pytest.mark.usefixtures('venv_active', 'simple_pkg')
def test_search(self):
"""Test results of pip search."""
out = run(pip_cmd('search', 'simple_pkg'), capture=True)
assert 'simple_pkg' in out