diff --git a/.travis.yml b/.travis.yml index da5df11..9e76bdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: 2.7 env: @@ -17,3 +18,4 @@ script: branches: except: - standalone + \ No newline at end of file diff --git a/README.rst b/README.rst index bfbf2ba..cf7db23 100644 --- a/README.rst +++ b/README.rst @@ -216,14 +216,21 @@ Running ``pypi-server -h`` will print a detailed usage message:: -a, --authenticate (UPDATE|download|list), ... comma-separated list of (case-insensitive) actions to authenticate - (requires giving also the -P option). For example to password-protect - package uploads & downloads while leaving listings public, give: - -a update,download. + Requires -P option and cannot not be empty unless -P is '.' + For example to password-protect package downloads (in addition to uploads) + while leaving listings public, give: + -P foo/htpasswd.txt -a update,download + To drop all authentications, use: + -P . -a '' + For example to password-protect package uploads & downloads while leaving + listings public, give: + -P -a update,download By default, only 'update' is password-protected. -P, --passwords PASSWORD_FILE use apache htpasswd file PASSWORD_FILE to set usernames & passwords used for authentication of certain actions (see -a option). + Set it explicitly to '.' to allow empty list of actions to authenticate. --disable-fallback disable redirect to real PyPI index for packages not found in the diff --git a/pypiserver/core.py b/pypiserver/core.py index fc12c8c..767c3db 100644 --- a/pypiserver/core.py +++ b/pypiserver/core.py @@ -209,14 +209,21 @@ pypi-server understands the following options: -a, --authenticate (UPDATE|download|list), ... comma-separated list of (case-insensitive) actions to authenticate - (requires giving also the -P option). For example to password-protect - package uploads & downloads while leaving listings public, give: - -a update,download. + Requires -P option and cannot not be empty unless -P is '.' + For example to password-protect package downloads (in addition to uploads) + while leaving listings public, give: + -P foo/htpasswd.txt -a update,download + To drop all authentications, use: + -P . -a '' + For example to password-protect package uploads & downloads while leaving + listings public, give: + -P -a update,download By default, only 'update' is password-protected. -P, --passwords PASSWORD_FILE use apache htpasswd file PASSWORD_FILE to set usernames & passwords used for authentication of certain actions (see -a option). + Set it explicitly to '.' to allow empty list of actions to authenticate. --disable-fallback disable redirect to real PyPI index for packages not found in the @@ -249,12 +256,12 @@ pypi-server understands the following options: --log-frmt the logging format-string. (see `logging.LogRecord` class from standard python library) - [Default: %(asctime)s|%(levelname)s|%(thread)d|%(message)s] + [Default: %(asctime)s|%(levelname)s|%(thread)d|%(message)s] --log-req-frmt FORMAT a format-string selecting Http-Request properties to log; set to '%s' to see them all. - [Default: %(bottle.request)s] - + [Default: %(bottle.request)s] + --log-res-frmt FORMAT a format-string selecting Http-Response properties to log; set to '%s' to see them all. [Default: %(status)s] @@ -354,7 +361,9 @@ def main(argv=None): if k in ("-p", "--port"): port = int(v) elif k in ("-a", "--authenticate"): - authenticated = [a.lower() for a in re.split("[, ]+", v.strip(" ,"))] + authenticated = [a.lower() + for a in re.split("[, ]+", v.strip(" ,")) + if a] actions = ("list", "download", "update") for a in authenticated: if a not in actions: @@ -408,8 +417,8 @@ def main(argv=None): usage() sys.exit(0) - if password_file and not (password_file and authenticated): - sys.exit("Must give both password file (-P) and actions to authenticate (-a).") + if password_file and password_file != '.' and not authenticated: + sys.exit("Actions to authenticate (-a) must not be empty, unless password file (-P) is '.'!") if len(roots) == 0: roots.append(os.path.expanduser("~/packages")) diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index e9e1602..28ee3c9 --- a/setup.py +++ b/setup.py @@ -14,8 +14,10 @@ except ImportError: if sys.version_info >= (3, 0): exec("def do_exec(co, loc): exec(co, loc)\n") + tests_require = [] else: exec("def do_exec(co, loc): exec co in loc\n") + tests_require = ['mock'] def get_version(): @@ -33,7 +35,8 @@ setup(name="pypiserver", version=get_version(), packages=["pypiserver"], package_data={'pypiserver': ['welcome.html']}, - url="https://github.com/pypiserver/pypiserver", + tests_require=tests_require, + url="https://github.com/pypiserver/pypiserver", maintainer="Ralf Schmitt, Kostis Anagnostopoulos", maintainer_email="ralf@systemexit.de, ankostis@gmail.com", classifiers=[ diff --git a/tests/test_main.py b/tests/test_main.py old mode 100755 new mode 100644 index 3a570eb..22b9bac --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,10 @@ import sys, os, pytest, logging from pypiserver import core +try: + from unittest import mock +except ImportError: + import mock class main_wrapper(object): @@ -111,3 +115,22 @@ def test_welcome_file(main): def test_welcome_file_default(main): main([]) assert "Welcome to pypiserver!" in main.app.module.config.welcome_msg + +def test_password_without_auth_list(main, monkeypatch): + sysexit = mock.MagicMock(side_effect=ValueError('BINGO')) + monkeypatch.setattr('sys.exit', sysexit) + with pytest.raises(ValueError) as excinfo: + main(["-P", "pswd-file", "-a", ""]) + assert excinfo.value.args[0] == 'BINGO' + +def test_password_alone(main, monkeypatch): + monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock()) + monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock()) + main(["-P", "pswd-file"]) + assert main.app.module.config.authenticated == ['update'] + +def test_dot_password_without_auth_list(main, monkeypatch): + monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock()) + monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock()) + main(["-P", ".", "-a", ""]) + assert main.app.module.config.authenticated == [] diff --git a/tox.ini b/tox.ini index 54e862c..c341ed6 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py25,py26,py27,py32,py33,py34 deps=pytest>=2.3 webtest beautifulsoup4 + mock commands=py.test [] sitepackages=False @@ -13,6 +14,7 @@ deps=pytest>=2.3 WebTest==1.4.3 WebOb==0.9.6.1 BeautifulSoup==3.2.1 + mock [pytest] norecursedirs = bin parts develop-eggs eggs .* _* CVS {args}