diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9b21d7e37..d02d917f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,10 @@ This document describes changes between each past release. 14.0.2 (unreleased) ------------------- +**New feature** + +- Add ability to disable explicit permissions at object level (ref #893). Use ``kinto.explicit_permissions = false`` to only rely on inherited permissions (see settings docs) + **Internal Changes** - Distinguish readonly errors in storage backend (``kinto.core.storage.exceptions.ReadonlyError``) diff --git a/kinto/core/__init__.py b/kinto/core/__init__.py index 58975b669..05589b52d 100644 --- a/kinto/core/__init__.py +++ b/kinto/core/__init__.py @@ -44,6 +44,7 @@ "eos": None, "eos_message": None, "eos_url": None, + "explicit_permissions": True, "error_info_link": "https://github.com/Kinto/kinto/issues/", "http_host": None, "http_scheme": None, @@ -155,7 +156,7 @@ def includeme(config): config.registry.heartbeats = {} # Public settings registry. - config.registry.public_settings = {"batch_max_requests", "readonly"} + config.registry.public_settings = {"batch_max_requests", "readonly", "explicit_permissions"} # Directive to declare arbitrary API capabilities. def add_api_capability(config, identifier, description="", url="", **kw): diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py index 8ef0b4bd6..5fe147b6d 100644 --- a/kinto/core/resource/__init__.py +++ b/kinto/core/resource/__init__.py @@ -15,6 +15,7 @@ HTTPServiceUnavailable, ) from pyramid.security import Everyone +from pyramid.settings import asbool from kinto.core import Service from kinto.core.errors import ERRORS, http_error, raise_invalid, request_GET, send_alert @@ -209,6 +210,7 @@ def __init__(self, request, context=None): parent_id=parent_id, current_principal=current_principal, prefixed_principals=request.prefixed_principals, + explicit_perm=asbool(request.registry.settings["explicit_permissions"]), ) # Initialize timestamp as soon as possible. diff --git a/kinto/core/resource/model.py b/kinto/core/resource/model.py index ce541dd89..e60834d25 100644 --- a/kinto/core/resource/model.py +++ b/kinto/core/resource/model.py @@ -35,6 +35,7 @@ def __init__( parent_id="", current_principal=None, prefixed_principals=None, + explicit_perm=True, ): """ :param storage: an instance of storage @@ -44,6 +45,12 @@ def __init__( :param str resource_name: the resource name :param str parent_id: the default parent id + :param bool explicit_perm: + Without explicit permissions, the ACLs on the object will + fully depend on the inherited permission tree (eg. read/write on parent). + This basically means that if user loose the permission on the + parent, they also loose the permission on children. + See https://github.com/Kinto/kinto/issues/893 """ self.storage = storage self.permission = permission @@ -52,6 +59,7 @@ def __init__( self.resource_name = resource_name self.current_principal = current_principal self.prefixed_principals = prefixed_principals + self.explicit_perm = explicit_perm # Object permission id. self.get_permission_object_id = None @@ -81,7 +89,8 @@ def _annotate(self, obj, perm_object_id): def _allow_write(self, perm_object_id): """Helper to give the ``write`` permission to the current user.""" - self.permission.add_principal_to_ace(perm_object_id, "write", self.current_principal) + if self.explicit_perm: + self.permission.add_principal_to_ace(perm_object_id, "write", self.current_principal) def get_objects( self, diff --git a/kinto/plugins/history/listener.py b/kinto/plugins/history/listener.py index bedeab748..dc6cdd0d0 100644 --- a/kinto/plugins/history/listener.py +++ b/kinto/plugins/history/listener.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from pyramid.settings import aslist +from pyramid.settings import asbool, aslist from kinto.core.utils import instance_uri @@ -109,6 +109,14 @@ def on_resource_changed(event): # Note: this will be rolledback if the transaction is rolledback. entry = storage.create(parent_id=bucket_uri, resource_name="history", obj=attrs) + # Without explicit permissions, the ACLs on the history entries will + # fully depend on the inherited permission tree (eg. bucket:read, bucket:write). + # This basically means that if user loose the permissions on the related + # object, they also loose the permission on the history entry. + # See https://github.com/Kinto/kinto/issues/893 + if not asbool(settings["explicit_permissions"]): + return + # The read permission on the newly created history entry is the union # of the object permissions with the one from bucket and collection. entry_principals = set(read_principals) diff --git a/tests/core/resource/test_base.py b/tests/core/resource/test_base.py index 71cbe374d..61b28605d 100644 --- a/tests/core/resource/test_base.py +++ b/tests/core/resource/test_base.py @@ -27,7 +27,7 @@ def test_raise_unavailable_if_fail_to_obtain_timestamp_with_readonly(self): excepted_exc = httpexceptions.HTTPServiceUnavailable - request.registry.settings = {"readonly": "true"} + request.registry.settings = {"readonly": "true", "explicit_permissions": "true"} with mock.patch.object( request.registry.storage, "resource_timestamp", diff --git a/tests/core/test_views_hello.py b/tests/core/test_views_hello.py index 1b9f57032..2ef504bea 100644 --- a/tests/core/test_views_hello.py +++ b/tests/core/test_views_hello.py @@ -32,7 +32,7 @@ def test_returns_eos_if_not_empty_in_settings(self): def test_public_settings_are_shown_in_view(self): response = self.app.get("/") settings = response.json["settings"] - expected = {"batch_max_requests": 25, "readonly": False} + expected = {"batch_max_requests": 25, "readonly": False, "explicit_permissions": True} self.assertEqual(expected, settings) def test_public_settings_can_be_set_from_registry(self):