diff --git a/pypiserver/__main__.py b/pypiserver/__main__.py index eacc7ba..809fcaa 100644 --- a/pypiserver/__main__.py +++ b/pypiserver/__main__.py @@ -197,17 +197,33 @@ def main(argv=None): err = sys.exc_info()[1] sys.exit("Invalid port(%r) due to: %s" % (v, err)) elif k in ("-a", "--authenticate"): - c.authenticated = [a.lower() - for a in re.split("[, ]+", v.strip(" ,")) - if a] - if c.authenticated == ['.']: - c.authenticated = [] + if '{' in v: + import ast + v = ast.literal_eval(v) + if isinstance(v, dict): + c.authenticated = {} + for user in v: + c.authenticated[user] = [a.lower() for a in v[user] if a] + if c.authenticated[user] == ['.']: + c.authenticated[user] = [] + else: + actions = ("list", "download", "update") + for a in c.authenticated[user]: + if a not in actions: + errmsg = "Action '%s' for option `%s` not one of %s!" + sys.exit(errmsg % (a, k, actions)) else: - actions = ("list", "download", "update") - for a in c.authenticated: - if a not in actions: - errmsg = "Action '%s' for option `%s` not one of %s!" - sys.exit(errmsg % (a, k, actions)) + c.authenticated = [a.lower() + for a in re.split("[, ]+", v.strip(" ,")) + if a] + if c.authenticated == ['.']: + c.authenticated = [] + else: + actions = ("list", "download", "update") + for a in c.authenticated: + if a not in actions: + errmsg = "Action '%s' for option `%s` not one of %s!" + sys.exit(errmsg % (a, k, actions)) elif k in ("-i", "--interface"): c.host = v elif k in ("-r", "--root"): diff --git a/pypiserver/_app.py b/pypiserver/_app.py index 02ad3da..2690386 100644 --- a/pypiserver/_app.py +++ b/pypiserver/_app.py @@ -42,17 +42,25 @@ class auth(object): def __call__(self, method): def protector(*args, **kwargs): - if self.action in config.authenticated: - if not request.auth or request.auth[1] is None: + auth = request.auth + if config.authenticated: + if not auth or auth[1] is None: raise HTTPError( - 401, headers={"WWW-Authenticate": 'Basic realm="pypi"'} - ) - if not config.auther(*request.auth): + 401, headers={"WWW-Authenticate": 'Basic realm="pypi"'}) + if not config.auther(*auth): + raise HTTPError(403) + if self.authorized(auth): + return method(*args, **kwargs) + else: raise HTTPError(403) return method(*args, **kwargs) - return protector + def authorized(self, auth): + if isinstance(config.authenticated, dict): + return auth[0] in config.authenticated and self.action in config.authenticated[auth[0]] + return self.action in config.authenticated + @app.hook('before_request') def log_request(): diff --git a/tests/test_app.py b/tests/test_app.py index a488b9e..f96c8c9 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -362,7 +362,7 @@ def test_no_cache_control_set(root, _app, testapp): def test_cache_control_set(root): from pypiserver import app AGE = 86400 - app_with_cache = webtest.TestApp(app(root=root.strpath, cache_control=AGE)) + app_with_cache = webtest.TestApp(app(root=root.strpath, cache_control=AGE, authenticated=[])) root.join("foo_bar-1.0.tar.gz").write("") resp = app_with_cache.get("/packages/foo_bar-1.0.tar.gz") assert "Cache-Control" in resp.headers @@ -381,8 +381,8 @@ def test_upload_badAction(root, testapp): assert "Unsupported ':action' field: BAD" in hp.unescape(resp.text) -@pytest.mark.parametrize(("package"), [f[0] - for f in test_core.files +@pytest.mark.parametrize(("package"), [f[0] + for f in test_core.files if f[1] and '/' not in f[0]]) def test_upload(package, root, testapp): resp = testapp.post("/", params={':action': 'file_upload'}, @@ -393,13 +393,13 @@ def test_upload(package, root, testapp): assert uploaded_pkgs[0].lower() == package.lower() -@pytest.mark.parametrize(("package"), [f[0] - for f in test_core.files +@pytest.mark.parametrize(("package"), [f[0] + for f in test_core.files if f[1] and '/' not in f[0]]) def test_upload_with_signature(package, root, testapp): resp = testapp.post("/", params={':action': 'file_upload'}, upload_files=[ - ('content', package, b''), + ('content', package, b''), ('gpg_signature', '%s.asc' % package, b'')]) assert resp.status_int == 200 uploaded_pkgs = [f.basename.lower() for f in root.listdir()] @@ -433,7 +433,7 @@ def test_remove_pkg_missingNaveVersion(name, version, root, testapp): params = {':action': 'remove_pkg', 'name': name, 'version': version} params = dict((k, v) for k,v in params.items() if v is not None) resp = testapp.post("/", expect_errors=1, params=params) - + assert resp.status == '400 Bad Request' assert msg %(name, version) in hp.unescape(resp.text) diff --git a/tests/test_main.py b/tests/test_main.py index e9ed4bf..d165967 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -169,3 +169,21 @@ def test_dot_password_without_auth_list(main, monkeypatch): main(["-P", ".", "-a", "."]) assert main.app.module.config.authenticated == [] + +def test_password_with_auth_list(main, monkeypatch): + monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock()) + monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock()) + main(["-P", "pswd-file", "-a", "update, download"]) + assert main.app.module.config.authenticated == ['update', 'download'] + +def test_password_with_auth_list_with_no_spaces(main, monkeypatch): + monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock()) + monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock()) + main(["-P", "pswd-file", "-a", "update,download"]) + assert main.app.module.config.authenticated == ['update', 'download'] + +def test_matrix_auth_list(main, monkeypatch): + monkeypatch.setitem(sys.modules, 'passlib', mock.MagicMock()) + monkeypatch.setitem(sys.modules, 'passlib.apache', mock.MagicMock()) + main(["-P", "pswd-file", "-a", "{'a': ['update', 'list'], 'b': ['download']}"]) + assert main.app.module.config.authenticated == {'a': ['update', 'list'], 'b': ['download']} diff --git a/tests/test_server.py b/tests/test_server.py index 5fc46aa..4ad34bc 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -43,7 +43,7 @@ Srv = namedtuple('Srv', ('proc', 'port', 'package')) def _run_server(packdir, port, authed, other_cli=''): pswd_opt_choices = { - True: "-Ptests/htpasswd.a.a -a update,download", + True: "-Ptests/htpasswd.a.a -a update,download,list", False: "-P. -a." } pswd_opts = pswd_opt_choices[authed]