Skip to content

Commit

Permalink
ZMI: re-implement the logic to prepend authentication path
Browse files Browse the repository at this point in the history
The previous approach from #1196 was not correct when using virtual host,
because AUTHENTICATION_PATH is not usable in virtual host contexts.

This uses a different approach, by making the js and css path
subscribers take care of generating the URLs with the authentication
path prepended using request API aware of virtual hosting.
  • Loading branch information
perrinjerome committed Apr 12, 2024
1 parent c33b799 commit 34108aa
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 28 deletions.
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
Fixes `#1077 <https://github.com/zopefoundation/Zope/issues/1077>`_.

- Fix authentication error viewing ZMI with a user defined outside of zope root.
Fixes `#1195 <https://github.com/zopefoundation/Zope/issues/1195>`_.
Fixes `#1195 <https://github.com/zopefoundation/Zope/issues/1195>`_ and
`#1203 <https://github.com/zopefoundation/Zope/issues/1195>`_.


5.9 (2023-11-24)
Expand Down
20 changes: 9 additions & 11 deletions src/App/dtml/manage_page_header.dtml
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,25 @@
</dtml-let>

<title><dtml-if title_or_id><dtml-var title_or_id><dtml-else>Zope</dtml-if></title>
<dtml-let basepath="'/'.join([''] + [p for p in (REQUEST['BASEPATH1'], REQUEST.get('AUTHENTICATION_PATH')) if p])">

<dtml-in css_urls>
<link rel="stylesheet" type="text/css" href="&dtml-basepath;&dtml-sequence-item;" />
<link rel="stylesheet" type="text/css" href="&dtml-sequence-item;" />
</dtml-in>
<dtml-in js_urls>
<script src="&dtml-basepath;&dtml-sequence-item;"></script>
<script src="&dtml-sequence-item;"></script>
</dtml-in>

<link rel="shortcut icon" type="image/x-icon" href="&dtml-basepath;/++resource++zmi/logo/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="&dtml-basepath;/++resource++zmi/logo/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="&dtml-basepath;/++resource++zmi/logo/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="&dtml-basepath;/++resource++zmi/logo/favicon/favicon-16x16.png" />
<link rel="manifest" href="&dtml-basepath;/++resource++zmi/logo/favicon/site.webmanifest" />
<link rel="mask-icon" href="&dtml-basepath;/++resource++zmi/logo/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-config" content="&dtml-basepath;/++resource++zmi/logo/favicon/browserconfig.xml"/>
<link rel="shortcut icon" type="image/x-icon" href="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/favicon-16x16.png" />
<link rel="manifest" href="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/site.webmanifest" />
<link rel="mask-icon" href="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-config" content="&dtml-BASEPATH1;/++resource++zmi/logo/favicon/browserconfig.xml"/>
<meta name="msapplication-TileColor" content="#2d89ef" />
<meta name="theme-color" content="#ffffff" />

</head>
</dtml-let>
<!-- REFACT what is a better way to get the last part of the current URL? -->
<body id="nodeid-<dtml-var "getId()">" class="zmi zmi-<dtml-var "this().meta_type.replace(' ', '-').replace('(', '').replace(')', '')"> zmi-<dtml-var "URL0[_.len(URL1)+1:]">">
</dtml-unless>
51 changes: 44 additions & 7 deletions src/zmi/styles/subscriber.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
import itertools

import zope.component
import zope.interface
from Acquisition import aq_parent
from ZPublisher.HTTPRequest import HTTPRequest


def prepend_authentication_path(request: HTTPRequest, path: str) -> str:
"""Prepend the path of the user folder.
Because ++resource++zmi is protected, we generate URLs relative to the
user folder of the logged-in user, so that the user can access the
resources.
"""
authenticated_user = request.get("AUTHENTICATED_USER")
if not authenticated_user:
return path

# prepend the authentication path, unless it is already part of the
# virtual host root.
authentication_path = []
ufpp = aq_parent(aq_parent(authenticated_user)).getPhysicalPath()
vrpp = request.get("VirtualRootPhysicalPath") or ()
for ufp, vrp in itertools.zip_longest(ufpp, vrpp):
if ufp == vrp:
continue
authentication_path.append(ufp)

parts = [
p for p in itertools.chain(authentication_path, path.split("/")) if p]

return request.physicalPathToURL(parts, relative=True)


@zope.component.adapter(zope.interface.Interface)
def css_paths(context):
"""Return paths to CSS files needed for the Zope 4 ZMI."""
return (
'/++resource++zmi/bootstrap-4.6.0/bootstrap.min.css',
'/++resource++zmi/fontawesome-free-5.15.2/css/all.css',
'/++resource++zmi/zmi_base.css',
prepend_authentication_path(context.REQUEST, path)
for path in (
'/++resource++zmi/bootstrap-4.6.0/bootstrap.min.css',
'/++resource++zmi/fontawesome-free-5.15.2/css/all.css',
'/++resource++zmi/zmi_base.css',
)
)


@zope.component.adapter(zope.interface.Interface)
def js_paths(context):
"""Return paths to JS files needed for the Zope 4 ZMI."""
return (
'/++resource++zmi/jquery-3.5.1.min.js',
'/++resource++zmi/bootstrap-4.6.0/bootstrap.bundle.min.js',
'/++resource++zmi/ace.ajax.org/ace.js',
'/++resource++zmi/zmi_base.js',
prepend_authentication_path(context.REQUEST, path)
for path in (
'/++resource++zmi/jquery-3.5.1.min.js',
'/++resource++zmi/bootstrap-4.6.0/bootstrap.bundle.min.js',
'/++resource++zmi/ace.ajax.org/ace.js',
'/++resource++zmi/zmi_base.js',
)
)
105 changes: 96 additions & 9 deletions src/zmi/styles/tests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Testing .subscriber.*"""

import Testing.ZopeTestCase
from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster
from Testing.ZopeTestCase.placeless import temporaryPlacelessSetUp
from Testing.ZopeTestCase.placeless import zcml
from zope.security.management import endInteraction
Expand All @@ -19,9 +22,16 @@ def setupZCML():


class SubscriberTests(Testing.ZopeTestCase.FunctionalTestCase):
"""Testing .subscriber.*"""
"""Test subscribers URL generation with a user from a user folder
not at the root.
"""
request_path = f"/{Testing.ZopeTestCase.folder_name}/manage_main"
resources_base_path = f"/{Testing.ZopeTestCase.folder_name}"

base_path = f'/{Testing.ZopeTestCase.folder_name}'
def afterSetUp(self):
if "virtual_hosting" not in self.app:
vhm = VirtualHostMonster()
vhm.addToContainer(self.app)

def call_manage_main(self):
"""Call /folder/manage_main and return the HTML text."""
Expand All @@ -30,9 +40,7 @@ def _call_manage_main(self):
# temporaryPlacelessSetUp insists in creating an interaction
# which the WSGI publisher does not expect.
endInteraction()
response = self.publish(
f'{self.base_path}/manage_main',
basic=basic_auth)
response = self.publish(self.request_path, basic=basic_auth)
return str(response)
return temporaryPlacelessSetUp(
_call_manage_main, required_zcml=setupZCML)(self)
Expand All @@ -41,12 +49,91 @@ def test_subscriber__css_paths__1(self):
"""The paths it returns are rendered in the ZMI."""
from .subscriber import css_paths
body = self.call_manage_main()
for path in css_paths(None):
self.assertIn(f'href="{self.base_path}{path}"', body)
for path in css_paths(self.folder):
self.assertIn(f'href="{self.resources_base_path}{path}"', body)

def test_subscriber__js_paths__1(self):
"""The paths it returns are rendered in the ZMI."""
from .subscriber import js_paths
body = self.call_manage_main()
for path in js_paths(None):
self.assertIn(f'src="{self.base_path}{path}"', body)
for path in js_paths(self.folder):
self.assertIn(f'src="{self.resources_base_path}{path}"', body)


class SubscriberTestsUserFromRootUserFolderViewingRootFolder(SubscriberTests):
"""Tests subscribers URL generation with a user from the root acl_users,
viewing the root of ZMI.
"""

request_path = "/manage_main"
resources_base_path = ""

def _setupFolder(self):
self.folder = self.app

def _setupUserFolder(self):
# we use the user folder from self.app
pass


class SubscriberTestsUserFromRootUserFolderViewingFolder(SubscriberTests):
"""Tests subscribers URL generation with a user from the root acl_users,
viewing a folder not at the root of ZMI. In such case, the URLs are not
relative to that folder, the resources are served from the root.
"""

request_path = f"/{Testing.ZopeTestCase.folder_name}/manage_main"
resources_base_path = ""

def _setupUser(self):
uf = self.app.acl_users
uf.userFolderAddUser(
Testing.ZopeTestCase.user_name,
Testing.ZopeTestCase.user_password,
["Manager"],
[],
)

def setRoles(self, roles, name=...):
# we set roles in _setupUser
pass

def login(self):
pass


class SubscriberUrlWithVirtualHostingTests(SubscriberTests):
"""Tests subscribers URL generation using virtual host."""

request_path = (
"/VirtualHostBase/https/example.org:443/VirtualHostRoot/"
f"{Testing.ZopeTestCase.folder_name}/manage_main"
)
resources_base_path = f"/{Testing.ZopeTestCase.folder_name}"


class SubscriberUrlWithVirtualHostingAndUserFolderInVirtualHostTests(
SubscriberTests):
"""Tests subscribers URL generation using virtual host, when
the authentication path is part of the virtual host base.
"""

request_path = (
"/VirtualHostBase/https/example.org:443/"
f"{Testing.ZopeTestCase.folder_name}/VirtualHostRoot/manage_main"
)
resources_base_path = ""


class SubscriberUrlWithVirtualHostingAndVHTests(SubscriberTests):
"""Tests subscribers URL generation using virtual host, when
the authentication path is part of the virtual host base and
using a "_vh_" path element.
"""

request_path = (
"/VirtualHostBase/https/example.org:443"
f"/{Testing.ZopeTestCase.folder_name}/"
"VirtualHostRoot/_vh_zz/manage_main"
)
resources_base_path = "/zz"

0 comments on commit 34108aa

Please sign in to comment.