diff --git a/doc/topics/access_control.rst b/doc/topics/access_control.rst index ae72758f..7836f533 100644 --- a/doc/topics/access_control.rst +++ b/doc/topics/access_control.rst @@ -99,13 +99,13 @@ read/write access to all packages, and can perform maintenance tasks. Whitespace-delimited list of users that belong to this group. Groups can have separately-defined read/write permissions on packages. -``auth.zero_security_mode`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``auth.zero_security_mode`` (deprecated) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Argument:** bool, optional -Run in a special, limited access-control mode. Any user with valid credentials -can upload any package. Everyone (even not-logged-in users) can view and -download all packages. (default False) +Replaced by ``pypi.default_read`` and ``pypi.default_write``. If enabled, will +set ``pypi.default_read = everyone`` and ``pypi.default_write = +authenticated``. SQL Database ------------ diff --git a/doc/topics/api.rst b/doc/topics/api.rst index 5a617cfb..10d74fbb 100644 --- a/doc/topics/api.rst +++ b/doc/topics/api.rst @@ -39,9 +39,10 @@ Upload a package ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Returns a webpage with all links to all versions of this package. -If :ref:`use fallback ` is enabled and the server does not -contain the package, this will return a ``302`` that points towards a fallback -server. +If :ref:`fallback ` is configured and the server does not contain the +package, this will return either a ``302`` that points towards the fallback +server (``redirect``), or a package index pulled from the fallback server +(``cache``). **Example**:: diff --git a/doc/topics/configuration.rst b/doc/topics/configuration.rst index ee887045..884fd235 100644 --- a/doc/topics/configuration.rst +++ b/doc/topics/configuration.rst @@ -5,10 +5,10 @@ This is a list of all configuration parameters for pypicloud PyPICloud ^^^^^^^^^ -.. _use_fallback: +.. _fallback: ``pypi.fallback`` -~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~ **Argument:** {'redirect', 'cache', 'none'}, optional This option defines what the behavior is when a requested package is not found @@ -35,6 +35,13 @@ http://pypi.python.org/simple) List of groups that are allowed to read packages that have no explicit user or group permissions (default ['authenticated']) +``pypi.default_write`` +~~~~~~~~~~~~~~~~~~~~~~ +**Argument:** list, optional + +List of groups that are allowed to write packages that have no explicit user or +group permissions (default no groups, only admin users) + ``pypi.cache_update`` ~~~~~~~~~~~~~~~~~~~~~ **Argument:** list, optional @@ -56,6 +63,12 @@ False) The HTTP Basic Auth realm (default 'pypi') +``pypi.use_fallback`` (deprecated) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**Argument:** bool, optional + +Replaced by ``pypi.fallback``. Setting to ``True`` has no effect. Setting to +``False`` will set ``pypi.fallback = none``. Storage ^^^^^^^ diff --git a/pypicloud/access/base.py b/pypicloud/access/base.py index 9ed1e76e..5fbb362c 100644 --- a/pypicloud/access/base.py +++ b/pypicloud/access/base.py @@ -24,16 +24,6 @@ def groups_to_principals(groups): return [group_to_principal(g) for g in groups] -def parse_principal(principal): - """ Parse a principal and return type, name """ - if principal == Everyone: - return ['group', 'everyone'] - elif principal == Authenticated: - return ['group', 'authenticated'] - else: - return principal.split(':', 1) - - class IAccessBackend(object): """ Base class for retrieving user and package permission data """ @@ -53,6 +43,7 @@ def configure(cls, settings): """ Configure the access backend with app settings """ cls.default_read = aslist(settings.get('pypi.default_read', ['authenticated'])) + cls.default_write = aslist(settings.get('pypi.default_write', [])) cls.cache_update = aslist(settings.get('pypi.cache_update', ['authenticated'])) @@ -74,10 +65,15 @@ def allowed_permissions(self, package): all_perms[group_to_principal(group)] = tuple(perms) # If there are no group or user specifications for the package, use the - # read-default + # default if len(all_perms) == 0: for principal in groups_to_principals(self.default_read): all_perms[principal] = ('read',) + for principal in groups_to_principals(self.default_write): + if principal in all_perms: + all_perms[principal] += ('write',) + else: + all_perms[principal] = ('write',) return all_perms def get_acl(self, package): diff --git a/pypicloud/access/config.py b/pypicloud/access/config.py index 4d110a1a..dabfeaac 100644 --- a/pypicloud/access/config.py +++ b/pypicloud/access/config.py @@ -1,12 +1,15 @@ """ Backend that reads access control rules from config file """ +import logging from collections import defaultdict -from pyramid.security import (Authenticated, Everyone, Allow, Deny, - ALL_PERMISSIONS) -from pyramid.settings import asbool, aslist +from pyramid.security import Everyone, Authenticated +from pyramid.settings import aslist, asbool from .base import IAccessBackend +LOG = logging.getLogger(__name__) + + class ConfigAccessBackend(IAccessBackend): """ Access Backend that uses values set in the config file """ @@ -14,24 +17,17 @@ class ConfigAccessBackend(IAccessBackend): @classmethod def configure(cls, settings): super(ConfigAccessBackend, cls).configure(settings) + if asbool(settings.get('auth.zero_security_mode', False)): + LOG.warn("Using deprecated option 'auth.zero_security_mode' " + "(replaced by 'pypi.default_read' and " + "'pypi.default_write'") + cls.default_read = [Everyone] + cls.default_write = [Authenticated] cls._settings = settings - cls.zero_security_mode = asbool(settings.get('auth.zero_security_mode', - False)) cls.admins = aslist(settings.get('auth.admins', [])) cls.user_groups = defaultdict(list) cls.group_map = {} - if cls.zero_security_mode: - cls.ROOT_ACL = [ - (Allow, Everyone, 'login'), - (Allow, Everyone, 'read'), - (Allow, Authenticated, 'write'), - (Allow, 'admin', ALL_PERMISSIONS), - (Deny, Everyone, ALL_PERMISSIONS), - ] - else: - cls.ROOT_ACL = IAccessBackend.ROOT_ACL - # Build dict that maps users to list of groups for key, value in settings.iteritems(): if not key.startswith('group.'): @@ -72,11 +68,7 @@ def _perms_from_short(value): def group_permissions(self, package, group=None): if group is not None: key = 'package.%s.group.%s' % (package, group) - perms = self._perms_from_short(self._settings.get(key)) - if (self.zero_security_mode and group == 'everyone' and - 'read' not in perms): - perms.append('read') - return perms + return self._perms_from_short(self._settings.get(key)) perms = {} group_prefix = 'package.%s.group.' % package for key, value in self._settings.iteritems(): @@ -84,10 +76,6 @@ def group_permissions(self, package, group=None): continue group = key[len(group_prefix):] perms[group] = self._perms_from_short(value) - if self.zero_security_mode: - perms.setdefault('everyone', []) - if 'read' not in perms['everyone']: - perms['everyone'].append('read') return perms def user_permissions(self, package, username=None): @@ -103,11 +91,6 @@ def user_permissions(self, package, username=None): perms[user] = self._perms_from_short(value) return perms - def get_acl(self, package): - if self.zero_security_mode: - return [] - return super(ConfigAccessBackend, self).get_acl(package) - def user_data(self, username=None): if username is not None: return { diff --git a/pypicloud/scripts.py b/pypicloud/scripts.py index c7426245..14746d6f 100644 --- a/pypicloud/scripts.py +++ b/pypicloud/scripts.py @@ -139,8 +139,6 @@ def bucket_validate(name): data['s3_bucket'] = prompt("S3 bucket name?", validate=bucket_validate) - data['db_url'] = 'sqlite:///%(here)s/db.sqlite' - data['encrypt_key'] = b64encode(os.urandom(32)) data['validate_key'] = b64encode(os.urandom(32)) @@ -148,7 +146,7 @@ def bucket_validate(name): data['password'] = _gen_password() data['session_secure'] = env == 'prod' - data['zero_security_mode'] = env != 'prod' + data['env'] = env if env == 'dev' or env == 'test': data['wsgi'] = 'waitress' diff --git a/pypicloud/templates/config.ini.jinja2 b/pypicloud/templates/config.ini.jinja2 index 451c821e..99a7f299 100644 --- a/pypicloud/templates/config.ini.jinja2 +++ b/pypicloud/templates/config.ini.jinja2 @@ -7,6 +7,13 @@ pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en +{% if env != 'prod' -%} +pypi.default_read = + everyone +pypi.default_write = + authenticated +{%- endif %} + pypi.storage = {{ storage }} {% if storage == 'file' -%} storage.dir = %(here)s/packages @@ -16,11 +23,7 @@ storage.secret_key = {{ secret_key }} storage.bucket = {{ s3_bucket }} {%- endif %} -db.url = {{ db_url }} - -{% if zero_security_mode -%} -auth.zero_security_mode = true -{%- endif %} +db.url = sqlite:///%(here)s/db.sqlite auth.admins = {{ admin }} diff --git a/pypicloud/util.py b/pypicloud/util.py index 067f2841..f868a7a9 100644 --- a/pypicloud/util.py +++ b/pypicloud/util.py @@ -1,9 +1,13 @@ """ Utilities """ -from distlib.util import split_filename +import posixpath + +import logging from distlib.locators import Locator, SimpleScrapingLocator +from distlib.util import split_filename from six.moves.urllib.parse import urlparse # pylint: disable=F0401,E0611 -import posixpath + +LOG = logging.getLogger(__name__) ALL_EXTENSIONS = Locator.source_extensions + Locator.binary_extensions @@ -69,8 +73,13 @@ def getdefaults(settings, *args): keys are found. """ + assert len(args) >= 3 args, default = args[:-1], args[-1] + canonical = args[0] for key in args: if key in settings: + if key != canonical: + LOG.warn("Using deprecated option '%s' " + "(replaced by '%s')", key, canonical) return settings[key] return default diff --git a/tests/test_access_backends.py b/tests/test_access_backends.py index fc9b1278..f1deebe8 100644 --- a/tests/test_access_backends.py +++ b/tests/test_access_backends.py @@ -8,7 +8,7 @@ from pypicloud.access import (IAccessBackend, IMutableAccessBackend, ConfigAccessBackend, RemoteAccessBackend, includeme, pwd_context) -from pypicloud.access.base import group_to_principal, parse_principal +from pypicloud.access.base import group_to_principal from pypicloud.access.sql import (SQLAccessBackend, User, UserPermission, association_table, GroupPermission, Group) from pypicloud.route import Root @@ -42,14 +42,6 @@ def test_group_to_principal_twice(self): g2 = group_to_principal(g1) self.assertEqual(g1, g2) - def test_parse_principal(self): - """ parse_principal returns type and value """ - self.assertEqual(parse_principal('user:aa'), ['user', 'aa']) - self.assertEqual(parse_principal('group:aa'), ['group', 'aa']) - self.assertEqual(parse_principal(Everyone), ['group', 'everyone']) - self.assertEqual(parse_principal(Authenticated), ['group', - 'authenticated']) - class BaseACLTest(unittest.TestCase): @@ -241,10 +233,18 @@ def test_admin_has_permission(self, u_userid): def test_has_permission_default_read(self): """ If no user/group permissions on a package, use default_read """ self.backend.default_read = ['everyone', 'authenticated'] + self.backend.default_write = [] perms = self.backend.allowed_permissions('anypkg') self.assertEqual(perms, {Everyone: ('read',), Authenticated: ('read',)}) + def test_has_permission_default_write(self): + """ If no user/group permissions on a package, use default_write """ + self.backend.default_read = ['authenticated'] + self.backend.default_write = ['authenticated'] + perms = self.backend.allowed_permissions('anypkg') + self.assertEqual(perms, {Authenticated: ('read', 'write')}) + def test_admin_principal(self): """ Admin user has the 'admin' principal """ access = IAccessBackend(None) @@ -433,57 +433,6 @@ def test_single_user_data(self): 'groups': ['foobars'], }) - def test_zero_security(self): - """ In zero_security_mode everyone has 'r' permission """ - settings = { - 'auth.zero_security_mode': True - } - self.backend.configure(settings) - can_read = self.backend.has_permission('floobydooby', 'read') - self.assertTrue(can_read) - - def test_zero_security_group(self): - """ In zero_security_mode 'everyone' group always can 'read' """ - settings = { - 'auth.zero_security_mode': True - } - self.backend.configure(settings) - perms = self.backend.group_permissions('floobydooby') - self.assertEqual(perms, { - 'everyone': ['read'], - }) - - @patch('pypicloud.access.base.effective_principals') - def test_zero_security_write(self, principals): - """ zero_security_mode has no impact on 'w' permission """ - settings = { - 'auth.zero_security_mode': True - } - self.backend.configure(settings) - principals.return_value = [Everyone] - self.assertTrue(self.backend.has_permission('pkg', 'read')) - self.assertFalse(self.backend.has_permission('pkg', 'write')) - - def test_root_acl_zero_sec(self): - """ Root ACL is super permissive in zero security mode """ - settings = { - 'auth.zero_security_mode': True - } - self.backend.configure(settings) - root = Root(self.request) - self.assert_allowed(root, 'login', ['admin', Everyone]) - self.assert_allowed(root, 'read', ['admin', Everyone]) - self.assert_allowed(root, 'write', ['admin', Authenticated]) - - def test_zero_security_mode_acl(self): - """ Zero security mode means ACL is empty """ - settings = { - 'auth.zero_security_mode': True - } - self.backend.configure(settings) - acl = self.backend.get_acl('mypkg') - self.assertEqual(acl, []) - def test_need_admin(self): """ Config backend is static and never needs admin """ self.backend.configure({})