Skip to content
This repository has been archived by the owner on Aug 27, 2023. It is now read-only.

Commit

Permalink
Adding default_write perms and deprecation warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
stevearc committed Mar 6, 2014
1 parent 0677873 commit c9aa57b
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 121 deletions.
10 changes: 5 additions & 5 deletions doc/topics/access_control.rst
Expand Up @@ -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
------------
Expand Down
7 changes: 4 additions & 3 deletions doc/topics/api.rst
Expand Up @@ -39,9 +39,10 @@ Upload a package
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Returns a webpage with all links to all versions of this package.

If :ref:`use fallback <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 <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**::

Expand Down
17 changes: 15 additions & 2 deletions doc/topics/configuration.rst
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
^^^^^^^
Expand Down
18 changes: 7 additions & 11 deletions pypicloud/access/base.py
Expand Up @@ -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 """
Expand All @@ -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']))

Expand All @@ -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):
Expand Down
43 changes: 13 additions & 30 deletions pypicloud/access/config.py
@@ -1,37 +1,33 @@
""" 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 """

@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.'):
Expand Down Expand Up @@ -72,22 +68,14 @@ 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():
if not key.startswith(group_prefix):
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):
Expand All @@ -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 {
Expand Down
4 changes: 1 addition & 3 deletions pypicloud/scripts.py
Expand Up @@ -139,16 +139,14 @@ 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))

data['admin'] = prompt("Admin username?")
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'
Expand Down
13 changes: 8 additions & 5 deletions pypicloud/templates/config.ini.jinja2
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
13 changes: 11 additions & 2 deletions 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


Expand Down Expand Up @@ -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
69 changes: 9 additions & 60 deletions tests/test_access_backends.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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({})
Expand Down

0 comments on commit c9aa57b

Please sign in to comment.