Repository: django-cms/django-cms

Issue:
"login_required" flag don't work via cms_apphook
There is only one function, which checks "login_required" flag - detail in "/site-packages/cms/views.py", which calls from "/site-packages/cms/urls.py" But when we add an app_hook to page - django loads only app_hook urls.py and ignored cms urls.py... And as the result - function 'detail' won't be called and checks will be ignored.. .



Discussion:
@digi604 moving https://github.com/divio/django-cms/blob/develop/cms/views.py#L137-139 before https://github.com/divio/django-cms/blob/develop/cms/views.py#L96 will allow using login_required as a quick way to restrict access to an application

If this works... go for it.

Ok, scrap my comment.
It won't work, obviously, as apphooks urls are statically loaded to global urlconf, and `cms.views.details` has no control over them.
We might provide a decorator for apphooks views (or urlconfs) to use CMS permission system to control access to the apphook urls.

At the end of the day django-cms should never enforce anything on the urls/views an apphook provides. There might be something completely different controlling permissions in an app based on whatever that app does.

But there is a common case for simple apps, where you'd just want the apphook views to obey to the page permissions as proposed in this ticket. So we should just make it as easy as possible as I propose in #3134 .


Resolving patch:
diff --git a/cms/app_base.py b/cms/app_base.py
--- a/cms/app_base.py
+++ b/cms/app_base.py
@@ -4,3 +4,4 @@ class CMSApp(object):
     urls = None
     menus = []
     app_name = None
+    permissions = True
diff --git a/cms/appresolver.py b/cms/appresolver.py
--- a/cms/appresolver.py
+++ b/cms/appresolver.py
@@ -213,17 +213,21 @@ def get_app_patterns():
         app = apphook_pool.get_apphook(title.page.application_urls)
         app_ns = app.app_name, title.page.application_namespace
         with force_language(title.language):
-            hooked_applications[title.page_id][title.language] = (app_ns, get_patterns_for_title(path, title))
+            hooked_applications[title.page_id][title.language] = (app_ns, get_patterns_for_title(path, title), app)
         included.append(mix_id)
         # Build the app patterns to be included in the cms urlconfs
     app_patterns = []
     for page_id in hooked_applications.keys():
         resolver = None
         for lang in hooked_applications[page_id].keys():
-            (app_ns, inst_ns), current_patterns = hooked_applications[page_id][lang]
+            (app_ns, inst_ns), current_patterns, app = hooked_applications[page_id][lang]
             if not resolver:
                 resolver = AppRegexURLResolver(r'', 'app_resolver', app_name=app_ns, namespace=inst_ns)
                 resolver.page_id = page_id
+            if app.permissions:
+                from cms.utils.decorators import cms_perms
+                for pat in current_patterns:
+                    pat._callback = cms_perms(pat.callback)
             extra_patterns = patterns('', *current_patterns)
             resolver.url_patterns_dict[lang] = extra_patterns
         app_patterns.append(resolver)
diff --git a/cms/utils/decorators.py b/cms/utils/decorators.py
new file mode 100644
--- /dev/null
+++ b/cms/utils/decorators.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from cms.views import _handle_no_page
+from django.contrib.auth.views import redirect_to_login
+from django.utils.http import urlquote
+from django.conf import settings
+
+
+def cms_perms(func):
+    def inner(request, *args, **kwargs):
+        page = request.current_page
+        if page:
+            if page.login_required and not request.user.is_authenticated():
+                return redirect_to_login(urlquote(request.get_full_path()), settings.LOGIN_URL)
+            if not page.has_view_permission(request):
+                return _handle_no_page(request, "$")
+        return func(request, *args, **kwargs)
+    return inner


Test patch:
diff --git a/cms/test_utils/project/sampleapp/cms_app.py b/cms/test_utils/project/sampleapp/cms_app.py
--- a/cms/test_utils/project/sampleapp/cms_app.py
+++ b/cms/test_utils/project/sampleapp/cms_app.py
@@ -7,6 +7,7 @@ class SampleApp(CMSApp):
     name = _("Sample App")
     urls = ["cms.test_utils.project.sampleapp.urls"]
     menus = [SampleAppMenu]
+    permissions = True
 
 apphook_pool.register(SampleApp)
 
diff --git a/cms/tests/apphooks.py b/cms/tests/apphooks.py
--- a/cms/tests/apphooks.py
+++ b/cms/tests/apphooks.py
@@ -192,6 +192,23 @@ def test_get_page_for_apphook(self):
 
             apphook_pool.clear()
 
+    def test_apphook_permissions(self):
+        with SettingsOverride(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests'):
+            en_title, de_title = self.create_base_structure(APP_NAME, ['en', 'de'])
+
+            with force_language("en"):
+                path = reverse('sample-settings')
+            response = self.client.get(path)
+            self.assertEqual(response.status_code, 200)
+            page = en_title.page.publisher_public
+            page.login_required = True
+            page.save()
+            page.publish('en')
+
+            response = self.client.get(path)
+            self.assertEqual(response.status_code, 302)
+            apphook_pool.clear()
+
     def test_get_page_for_apphook_on_preview_or_edit(self):
 
         if get_user_model().USERNAME_FIELD == 'email':