Repository: plone/plone.restapi (language: python)

Issue:
Login endpoint sets bad cookie for Zope-level user
Steps to reproduce:
1. Log in to volto as a Zope-level admin user
2. Create a new page titled Test and leave it in the private workflow state
3. Go to /++api++/test

Expected: the JSON response for the content endpoint is displayed
Actual: Unauthorized

The problem seems to be that for Zope-level users, the `__ac` cookie is set by the credentials_cookie_auth plugin in the root-level PAS, instead of the session plugin in the Plone-level PAS. But credentials_cookie_auth needs access to the user's password to set the cookie correctly, and it is getting lost in the chain from the login endpoint -> PlonePAS -> PAS updateCredentials.

Bugfix incoming.


Discussion / hints:

Fix patch:
diff --git a/news/1572.bugfix b/news/1572.bugfix
new file mode 100644
index 0000000000..316c6b1a61
--- /dev/null
+++ b/news/1572.bugfix
@@ -0,0 +1,1 @@
+Fix bug where the `@login` endpoint did not set the correct `__ac` cookie for Zope users. [davisagli]
diff --git a/src/plone/restapi/services/auth/login.py b/src/plone/restapi/services/auth/login.py
index 47813440d5..4bc47df6ef 100644
--- a/src/plone/restapi/services/auth/login.py
+++ b/src/plone/restapi/services/auth/login.py
@@ -32,6 +32,11 @@ def reply(self):
         password = data["password"]
         uf = self._find_userfolder(userid)
 
+        # Also put the password in __ac_password on the request.
+        # The post-login code in PlonePAS expects to find it there
+        # when it calls the PAS updateCredentials plugin.
+        self.request.form["__ac_password"] = data["password"]
+
         if uf is not None:
             plugins = uf._getOb("plugins")
             authenticators = plugins.listPlugins(IAuthenticationPlugin)


Test patch:
diff --git a/src/plone/restapi/tests/test_functional_auth.py b/src/plone/restapi/tests/test_functional_auth.py
index e308c903f7..ea2f5f4704 100644
--- a/src/plone/restapi/tests/test_functional_auth.py
+++ b/src/plone/restapi/tests/test_functional_auth.py
@@ -1,4 +1,3 @@
-from plone.app.testing import login
 from plone.app.testing import setRoles
 from plone.app.testing import SITE_OWNER_NAME
 from plone.app.testing import SITE_OWNER_PASSWORD
@@ -21,7 +20,6 @@ def setUp(self):
         self.portal = self.layer["portal"]
         self.portal_url = self.portal.absolute_url()
         setRoles(self.portal, TEST_USER_ID, ["Manager"])
-        login(self.portal, SITE_OWNER_NAME)
         self.private_document = self.portal[
             self.portal.invokeFactory("Document", id="doc1", title="My Document")
         ]
@@ -68,12 +66,21 @@ def test_api_login_grants_zmi(self):
         """
         Logging in via the API also grants access to the Zope root ZMI.
         """
+        app = self.layer["app"]
+        app.acl_users.plugins.users.addUser(
+            "zopeuser",
+            "zopeuser",
+            TEST_USER_PASSWORD,
+        )
+        app.acl_users.plugins.roles.assignRoleToPrincipal("Manager", "zopeuser")
+        transaction.commit()
+
         session = requests.Session()
         self.addCleanup(session.close)
         login_resp = session.post(
             self.portal_url + "/@login",
             headers={"Accept": "application/json"},
-            json={"login": SITE_OWNER_NAME, "password": SITE_OWNER_PASSWORD},
+            json={"login": "zopeuser", "password": TEST_USER_PASSWORD},
         )
         self.assertIn(
             "__ac",
@@ -94,11 +101,6 @@ def test_api_login_grants_zmi(self):
         zmi_resp = session.get(
             self.layer["app"].absolute_url() + "/manage_workspace",
         )
-        # Works in the browser when running `$ bin/instance fg` in a `plone.restapi`
-        # checkout against `http://localhost:8080/manage_main` but doesn't work in the
-        # browser against the test fixture at `http://localhost:55001/manage_main`.  My
-        # guess is that there's some subtle difference in the PAS plugin configuration.
-        self.skipTest("FIXME: Works in real instance but not test fixture")
         self.assertEqual(
             zmi_resp.status_code,
             200,