1
0
mirror of https://github.com/pypiserver/pypiserver synced 2024-11-09 16:45:51 +01:00

Merge pull request #66 from koodaamo/auth_improvements

Merge Auth improvements (directly on github).
This commit is contained in:
Kostis Anagnostopoulos 2015-02-15 21:52:14 +01:00
commit 202c26fb75
4 changed files with 70 additions and 12 deletions

@ -88,9 +88,16 @@ pypi-server -h will print a detailed usage message::
-i INTERFACE, --interface INTERFACE
listen on interface INTERFACE (default: 0.0.0.0, any interface)
-a (update|download|list), ... --authenticate (update|download|list), ...
comma-separated list of actions to authenticate (requires giving also
the -P option). For example to password-protect package uploads and
downloads while leaving listings public, give: -a update,download.
Note: make sure there is no space around the comma(s); otherwise, an
error will occur.
-P PASSWORD_FILE, --passwords PASSWORD_FILE
use apache htpasswd file PASSWORD_FILE in order to enable password
protected uploads.
use apache htpasswd file PASSWORD_FILE to set usernames & passwords
used for authentication (requires giving the -s option as well).
--disable-fallback
disable redirect to real PyPI index for packages not found in the
@ -468,6 +475,13 @@ proxypypi (https://pypi.python.org/pypi/proxypypi)
Changelog
=========
1.1.7 (2015-01-10)
------------------
- support password protected package listings and downloads,
in addition to uploads (use the -a, --authenticate option
to specify which to protect).
1.1.6 (2014-03-05)
------------------
- remove --index-url cli parameter introduced in 1.1.5

@ -5,6 +5,7 @@ version = __version__ = "1.1.6"
def app(root=None,
redirect_to_fallback=True,
fallback_url=None,
authenticated=[],
password_file=None,
overwrite=False,
log_req_frmt="%(bottle.request)s",
@ -25,7 +26,7 @@ def app(root=None,
fallback_url = "http://pypi.python.org/simple"
_app.configure(root=root, redirect_to_fallback=redirect_to_fallback, fallback_url=fallback_url,
password_file=password_file, overwrite=overwrite,
authenticated=authenticated, password_file=password_file, overwrite=overwrite,
log_req_frmt=log_req_frmt, log_res_frmt=log_res_frmt, log_err_frmt=log_err_frmt)
_app.app.module = _app

@ -33,9 +33,29 @@ def validate_user(username, password):
return config.htpasswdfile.check_password(username, password)
class auth(object):
"decorator to apply authentication if specified for the decorated method & action"
def __init__(self, action):
self.action = action
def __call__(self, method):
def protector(*args, **kwargs):
if self.action in config.authenticated:
if not request.auth or request.auth[1] is None:
raise HTTPError(401, header={"WWW-Authenticate": 'Basic realm="pypi"'})
if not validate_user(*request.auth):
raise HTTPError(403)
return method(*args, **kwargs)
return protector
def configure(root=None,
redirect_to_fallback=True,
fallback_url=None,
authenticated=[],
password_file=None,
overwrite=False,
log_req_frmt=None,
@ -46,12 +66,15 @@ def configure(root=None,
log.info("Starting(%s)", dict(root=root,
redirect_to_fallback=redirect_to_fallback,
fallback_url=fallback_url,
authenticated=authenticated,
password_file=password_file,
overwrite=overwrite,
log_req_frmt=log_req_frmt,
log_res_frmt=log_res_frmt,
log_err_frmt=log_err_frmt))
config.authenticated = authenticated
if root is None:
root = os.path.expanduser("~/packages")
@ -146,13 +169,8 @@ easy_install -i %(URL)ssimple/ PACKAGE
@app.post('/')
@auth("update")
def update():
if not request.auth or request.auth[1] is None:
raise HTTPError(401, header={"WWW-Authenticate": 'Basic realm="pypi"'})
if not validate_user(*request.auth):
raise HTTPError(403)
try:
action = request.forms[':action']
except KeyError:
@ -208,11 +226,13 @@ def update():
@app.route("/simple")
@auth("list")
def simpleindex_redirect():
return redirect(request.fullpath + "/")
@app.route("/simple/")
@auth("list")
def simpleindex():
res = ["<html><head><title>Simple Index</title></head><body>\n"]
for x in sorted(get_prefixes(packages())):
@ -223,6 +243,7 @@ def simpleindex():
@app.route("/simple/:prefix")
@app.route("/simple/:prefix/")
@auth("list")
def simple(prefix=""):
fp = request.fullpath
if not fp.endswith("/"):
@ -246,6 +267,7 @@ def simple(prefix=""):
@app.route('/packages')
@app.route('/packages/')
@auth("list")
def list_packages():
fp = request.fullpath
if not fp.endswith("/"):
@ -262,6 +284,7 @@ def list_packages():
@app.route('/packages/:filename#.*#')
@auth("download")
def server_static(filename):
entries = find_packages(packages())
for x in entries:

@ -189,9 +189,16 @@ pypi-server understands the following options:
-i INTERFACE, --interface INTERFACE
listen on interface INTERFACE (default: 0.0.0.0, any interface)
-a (update|download|list), ... --authenticate (update|download|list), ...
comma-separated list of actions to authenticate (requires giving also
the -P option). For example to password-protect package uploads and
downloads while leaving listings public, give: -a update,download.
Note: make sure there is no space around the comma(s); otherwise, an
error will occur.
-P PASSWORD_FILE, --passwords PASSWORD_FILE
use apache htpasswd file PASSWORD_FILE in order to enable password
protected uploads.
use apache htpasswd file PASSWORD_FILE to set usernames & passwords
used for authentication (requires giving the -s option as well).
--disable-fallback
disable redirect to real PyPI index for packages not found in the
@ -275,6 +282,7 @@ def main(argv=None):
server = DEFAULT_SERVER
redirect_to_fallback = True
fallback_url = "http://pypi.python.org/simple"
authenticated = []
password_file = None
overwrite = False
verbosity = 1
@ -289,9 +297,10 @@ def main(argv=None):
update_stable_only = True
try:
opts, roots = getopt.getopt(argv[1:], "i:p:r:d:P:Uuvxoh", [
opts, roots = getopt.getopt(argv[1:], "i:p:a:r:d:P:Uuvxoh", [
"interface=",
"passwords=",
"authenticate=",
"port=",
"root=",
"server=",
@ -313,6 +322,13 @@ def main(argv=None):
for k, v in opts:
if k in ("-p", "--port"):
port = int(v)
elif k in ("-a", "--authenticate"):
authenticated = [a.strip() for a in v.strip(',').split(',')]
actions = ("list", "download", "update")
for a in authenticated:
if a not in actions:
errmsg = "Incorrect action '%s' given with option '%s'" % (a, k)
sys.exit(errmsg)
elif k in ("-i", "--interface"):
host = v
elif k in ("-r", "--root"):
@ -357,6 +373,9 @@ def main(argv=None):
usage()
sys.exit(0)
if (password_file or authenticated) and not (password_file and authenticated):
sys.exit("Must give both password file (-P) and actions to authenticate (-a).")
if len(roots) == 0:
roots.append(os.path.expanduser("~/packages"))
@ -375,6 +394,7 @@ def main(argv=None):
a = app(
root=roots,
redirect_to_fallback=redirect_to_fallback,
authenticated=authenticated,
password_file=password_file,
fallback_url=fallback_url,
overwrite=overwrite,