diff --git a/.gitignore b/.gitignore index 0b7f2b3..2c804ac 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ var/ /venv/ .installed.txt .lock +.mxdev_cache ## # Add extra configuration options in .meta.toml: diff --git a/CHANGES.md b/CHANGES.md index 787c140..735a1ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ -## 1.3.2 (unreleased) +## 1.4.0 (unreleased) +- Upgrade dev environment to Plone 6.1.3 + [remdub] + +- Override views related to user management + We no longer create or modify users in Plone + This is now handled by Keycloak + [remdub] - Remove deprecated methods related to redirect uris We are not using those methods anymore since 1.3.0 diff --git a/constraints.txt b/constraints.txt index 9569987..b795763 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1 @@ --c https://dist.plone.org/release/6.1.1/constraints.txt +-c https://dist.plone.org/release/6.1.3/constraints.txt diff --git a/mx.ini b/mx.ini index 9fccb99..753119b 100644 --- a/mx.ini +++ b/mx.ini @@ -15,3 +15,7 @@ main-package = -e .[test] ; pushurl = git@github.com:collective/example.contenttype.git ; extras = test ; branch = feature-7 +[pas.plugins.oidc] +url = https://github.com/collective/pas.plugins.oidc.git +pushurl = git@github.com:collective/pas.plugins.oidc.git +branch = main diff --git a/src/pas/plugins/kimug/browser/configure.zcml b/src/pas/plugins/kimug/browser/configure.zcml index 371b2de..c6471a4 100644 --- a/src/pas/plugins/kimug/browser/configure.zcml +++ b/src/pas/plugins/kimug/browser/configure.zcml @@ -75,5 +75,37 @@ layer="pas.plugins.kimug.interfaces.IBrowserLayer" /> + + + + + + + diff --git a/src/pas/plugins/kimug/browser/usergroups_usersoverview.pt b/src/pas/plugins/kimug/browser/usergroups_usersoverview.pt new file mode 100644 index 0000000..2a0f069 --- /dev/null +++ b/src/pas/plugins/kimug/browser/usergroups_usersoverview.pt @@ -0,0 +1,289 @@ + + + + + + + + +
+ +

Users

+ +
+ Portal status message +
+ +
+ Note that roles set here apply directly to a user. + The symbol + + indicates a role inherited from membership in a group. +
+
+ +
+ +
+

+ Note + Some or all of your PAS user source + plugins do not allow listing of users, so you may not see + the users defined by those plugins unless doing a specific + search. +

+ +
+ + + +
+ Add New User + User Search + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
User name
Role
+ + ${user/fullname} + (${user/login}) + + + + + + + + + + +
No matches + Enter a username to search for + + Enter a username to search for, or click 'Show All' +
+ +
+ + + + + + + +
+ +
+ +
+
+ + + +
+
+
+ +
+ +
+ + + diff --git a/src/pas/plugins/kimug/browser/view.py b/src/pas/plugins/kimug/browser/view.py index e7ede3b..e46525e 100644 --- a/src/pas/plugins/kimug/browser/view.py +++ b/src/pas/plugins/kimug/browser/view.py @@ -138,3 +138,25 @@ def __call__(self): self.request.response.redirect(return_url) else: raise Unauthorized() + + +class NewUserView(BrowserView): + def __call__(self): + url_to_redirect = self.context["acl_users"]["oidc"].getProperty("add_user_url") + self.request.response.redirect(url_to_redirect) + + +class PersonalInformationView(BrowserView): + def __call__(self): + url_to_redirect = self.context["acl_users"]["oidc"].getProperty( + "personal_information_url" + ) + self.request.response.redirect(url_to_redirect) + + +class ChangePasswordView(BrowserView): + def __call__(self): + url_to_redirect = self.context["acl_users"]["oidc"].getProperty( + "change_password_url" + ) + self.request.response.redirect(url_to_redirect) diff --git a/src/pas/plugins/kimug/interfaces.py b/src/pas/plugins/kimug/interfaces.py index 9380658..1f89ab5 100644 --- a/src/pas/plugins/kimug/interfaces.py +++ b/src/pas/plugins/kimug/interfaces.py @@ -145,3 +145,24 @@ class IKimugSettings(Interface): values=["GET", "POST"], default="POST", ) + + add_user_url = schema.TextLine( + title=_("Add User URL"), + description=_("URL to redirect users to create a new account."), + required=True, + default="http://localhost/wca/", + ) + + personal_information_url = schema.TextLine( + title=_("Personal Information URL"), + description=_("URL to redirect users to manage their personal information."), + required=True, + default="http://localhost/wca/profile/", + ) + + change_password_url = schema.TextLine( + title=_("Change password URL"), + description=_("URL to redirect users to change their password."), + required=True, + default="http://localhost/wca/profile/", + ) diff --git a/src/pas/plugins/kimug/plugin/__init__.py b/src/pas/plugins/kimug/plugin/__init__.py index 0d28b34..aec1794 100644 --- a/src/pas/plugins/kimug/plugin/__init__.py +++ b/src/pas/plugins/kimug/plugin/__init__.py @@ -38,6 +38,36 @@ class KimugPlugin(OIDCPlugin): meta_type = "Kimug Plugin" _dont_swallow_my_exceptions = True + add_user_url: str = "" + personal_information_url: str = "" + change_password_url: str = "" + _properties = list(OIDCPlugin._properties) + _properties.append( + { + "id": "add_user_url", + "type": "string", + "mode": "w", + "label": "Add User URL", + } + ) + _properties.append( + { + "id": "personal_information_url", + "type": "string", + "mode": "w", + "label": "Personal Information URL", + } + ) + _properties.append( + { + "id": "change_password_url", + "type": "string", + "mode": "w", + "label": "Change Password URL", + } + ) + _properties = tuple(_properties) + @security.private def getRolesForPrincipal(self, user, request=None): """Fulfill RolesPlugin requirements""" diff --git a/src/pas/plugins/kimug/utils.py b/src/pas/plugins/kimug/utils.py index fe4753f..63c86c1 100644 --- a/src/pas/plugins/kimug/utils.py +++ b/src/pas/plugins/kimug/utils.py @@ -55,6 +55,16 @@ def set_oidc_settings(context): oidc.scope = ("openid", "profile", "email") oidc.userinfo_endpoint_method = "GET" + oidc.add_user_url = os.environ.get( + "keycloak_add_user_url", "http://localhost/wca/" + ) + oidc.personal_information_url = os.environ.get( + "keycloak_personal_information_url", "http://localhost/wca/profile/" + ) + oidc.change_password_url = os.environ.get( + "keycloak_change_password_url", "http://localhost/wca/change_password/" + ) + api.portal.set_registry_record( "plone.external_login_url", "acl_users/oidc/login" ) diff --git a/tests/conftest.py b/tests/conftest.py index 2f44d7f..5e5e0e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,6 +59,9 @@ def keycloak(keycloak_service): "client_id": "plone", "client_secret": "12345678910", # nosec B105 "scope": ("openid", "profile", "email"), + "add_user_url": "http://kamoulox.be/add_user", + "personal_information_url": "http://kamoulox.be/personal_info", + "change_password_url": "http://kamoulox.be/change_password", } diff --git a/tests/view/test_change_password.py b/tests/view/test_change_password.py new file mode 100644 index 0000000..7b3c14c --- /dev/null +++ b/tests/view/test_change_password.py @@ -0,0 +1,27 @@ +from plone import api +from zope.component import getMultiAdapter + +import pytest + + +class TestNewUserView: + @pytest.fixture(autouse=True) + def _init(self, portal, http_request): + with api.env.adopt_roles( + [ + "Manager", + ] + ): + self.portal = portal + self.request = http_request + view = getMultiAdapter((self.portal, self.request), name="change-password") + self.view = view + + def test_redirect(self): + """Test the redirect after going to change password.""" + self.view() + assert self.view.request.response.status == 302 + assert ( + self.view.request.response.headers["location"] + == "http://kamoulox.be/change_password" + ) diff --git a/tests/view/test_new_user.py b/tests/view/test_new_user.py new file mode 100644 index 0000000..de3b4c3 --- /dev/null +++ b/tests/view/test_new_user.py @@ -0,0 +1,27 @@ +from plone import api +from zope.component import getMultiAdapter + +import pytest + + +class TestNewUserView: + @pytest.fixture(autouse=True) + def _init(self, portal, http_request): + with api.env.adopt_roles( + [ + "Manager", + ] + ): + self.portal = portal + self.request = http_request + view = getMultiAdapter((self.portal, self.request), name="new-user") + self.view = view + + def test_redirect(self): + """Test the redirect after creating a new user.""" + self.view() + assert self.view.request.response.status == 302 + assert ( + self.view.request.response.headers["location"] + == "http://kamoulox.be/add_user" + ) diff --git a/tests/view/test_personal_information.py b/tests/view/test_personal_information.py new file mode 100644 index 0000000..1cce983 --- /dev/null +++ b/tests/view/test_personal_information.py @@ -0,0 +1,29 @@ +from plone import api +from zope.component import getMultiAdapter + +import pytest + + +class TestNewUserView: + @pytest.fixture(autouse=True) + def _init(self, portal, http_request): + with api.env.adopt_roles( + [ + "Manager", + ] + ): + self.portal = portal + self.request = http_request + view = getMultiAdapter( + (self.portal, self.request), name="personal-information" + ) + self.view = view + + def test_redirect(self): + """Test the redirect after going to personal information.""" + self.view() + assert self.view.request.response.status == 302 + assert ( + self.view.request.response.headers["location"] + == "http://kamoulox.be/personal_info" + ) diff --git a/tests/view/test_usergroup_userprefs.py b/tests/view/test_usergroup_userprefs.py new file mode 100644 index 0000000..e424949 --- /dev/null +++ b/tests/view/test_usergroup_userprefs.py @@ -0,0 +1,49 @@ +from bs4 import BeautifulSoup +from plone import api +from zope.component import getMultiAdapter + +import pytest + + +class TestUsergroupUserPrefsView: + @pytest.fixture(autouse=True) + def _init(self, portal, http_request): + with api.env.adopt_roles( + [ + "Manager", + ] + ): + self.portal = portal + self.request = http_request + view = getMultiAdapter( + (self.portal, self.request), name="usergroup-userprefs" + ) + self.soup = BeautifulSoup(view(), "html.parser") + + def test_removed_columns(self): + """Test the removed columns of the user groups overview.""" + table_userlisting = self.soup.find("table", summary="User Listing") + headers = table_userlisting.find_all("th") + """ + User name, +
Contributor
, +
Editor
, +
Member
, +
Reader
, +
Reviewer
, +
Site Administrator
, +
Manager
+ """ + assert len(headers) == 8 + expected_headers = [ + "User name", + "Contributor", + "Editor", + "Member", + "Reader", + "Reviewer", + "Site Administrator", + "Manager", + ] + for index, header in enumerate(headers): + assert header.get_text(strip=True) == expected_headers[index]