This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/master by this push:
     new 14a082a6b [OPENMEETINGS-2843] group admin is limited to manage only 
regular users in own group
14a082a6b is described below

commit 14a082a6b8c8dac011c65521db2555ce98f81de1
Author: Maxim Solodovnik <[email protected]>
AuthorDate: Fri Oct 9 14:49:05 2026 +0700

    [OPENMEETINGS-2843] group admin is limited to manage only regular users in 
own group
---
 LICENSE                                            |  49 ++++---
 .../apache/openmeetings/db/entity/user/User.java   |   4 +-
 .../openmeetings/web/admin/extra/ExtraForm.java    |   4 +-
 .../openmeetings/web/admin/groups/GroupForm.java   |  23 ++--
 .../openmeetings/web/admin/rooms/RoomForm.java     |  12 +-
 .../openmeetings/web/admin/users/UserForm.java     |  76 ++++++++---
 .../apache/openmeetings/web/app/WebSession.java    |   4 +
 .../openmeetings/web/common/GeneralUserForm.java   | 127 +++++++++++++-----
 .../openmeetings/web/common/general-user-form.js   |  32 +++++
 .../web/room/menu/RoomInvitationForm.java          |   4 +-
 .../web/user/calendar/AppointmentDialog.java       |   4 +-
 .../openmeetings/web/util/OmSelect2Choice.java     |  33 +++++
 ...rMultiChoice.java => OmSelect2MultiChoice.java} |  27 ++--
 .../openmeetings/web/util/UserMultiChoice.java     |   3 +-
 .../apache/openmeetings/web/GroupAdminTest.java    | 147 +++++++++++++++++++++
 pom.xml                                            |   4 +-
 16 files changed, 431 insertions(+), 122 deletions(-)

diff --git a/LICENSE b/LICENSE
index 27ff7e5ad..6d8623be5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -319,7 +319,7 @@ conditions of the following licenses.
         * Microsoft JDBC Driver for SQL Server 
(com.microsoft.sqlserver:mssql-jdbc:13.6.0.jre11 - 
https://github.com/Microsoft/mssql-jdbc)
         * minimasonry (org.webjars.npm:minimasonry:1.3.2 - 
https://www.webjars.org)
         * Modernizr (org.webjars:modernizr:2.8.3-1 - http://webjars.org)
-        * moment (org.webjars.npm:moment:2.30.1 - https://www.webjars.org)
+        * moment (org.webjars.npm:moment:2.31.0 - https://www.webjars.org)
         * org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec)
         * org.danekja.jdk-serializable-functional 
(org.danekja:jdk-serializable-functional:1.9.0 - 
https://github.com/danekja/jdk-serializable-functional)
         * progressbar.js (org.webjars.npm:progressbar.js:1.1.1 - 
https://www.webjars.org)
@@ -331,7 +331,7 @@ conditions of the following licenses.
         * Tempus Dominus Bootstrap 4 
(org.webjars:tempusdominus-bootstrap-4:5.39.0 - http://webjars.org)
         * typeahead.js (org.webjars:typeaheadjs:0.10.5-1 - http://webjars.org)
         * uuid (org.webjars.npm:uuid:9.0.0-beta.0 - https://www.webjars.org)
-        * wicketstuff-urlfragment 
(org.wicketstuff:wicketstuff-urlfragment:10.11.0 - 
https://github.com/Horsed/WicketUrlFragment/wicketstuff-urlfragment)
+        * wicketstuff-urlfragment 
(org.wicketstuff:wicketstuff-urlfragment:10.12.0-SNAPSHOT - 
https://github.com/Horsed/WicketUrlFragment/wicketstuff-urlfragment)
 
     NOASSERTION
 
@@ -433,8 +433,8 @@ conditions of the following licenses.
         * Apache Tika XML parser module 
(org.apache.tika:tika-parser-xml-module:4.1.0 - 
https://tika.apache.org/tika-parser-xml-module/)
         * Apache Tika XMP commons 
(org.apache.tika:tika-parser-xmp-commons:4.1.0 - 
https://tika.apache.org/tika-parser-xmp-commons/)
         * Apache Tika ZIP commons 
(org.apache.tika:tika-parser-zip-commons:4.1.0 - 
https://tika.apache.org/tika-parser-zip-commons/)
-        * Apache Wicket IPageStore Common 
(org.wicketstuff:wicketstuff-datastore-common:10.11.0 - 
http://wicketstuff.org/datastores-parent/wicketstuff-datastore-common)
-        * Apache Wicket IPageStore Hazelcast 
(org.wicketstuff:wicketstuff-datastore-hazelcast:10.11.0 - 
http://wicketstuff.org/datastores-parent/wicketstuff-datastore-hazelcast)
+        * Apache Wicket IPageStore Common 
(org.wicketstuff:wicketstuff-datastore-common:10.12.0-SNAPSHOT - 
http://wicketstuff.org/datastores-parent/wicketstuff-datastore-common)
+        * Apache Wicket IPageStore Hazelcast 
(org.wicketstuff:wicketstuff-datastore-hazelcast:10.12.0-SNAPSHOT - 
http://wicketstuff.org/datastores-parent/wicketstuff-datastore-hazelcast)
         * Apache XBean :: ASM shaded (repackaged) 
(org.apache.xbean:xbean-asm9-shaded:4.26 - 
http://geronimo.apache.org/maven/xbean/4.26/xbean-asm9-shaded)
         * Asterisk-Java (org.asteriskjava:asterisk-java:3.42.2 - 
https://github.com/asterisk-java/asterisk-java)
         * Bootstrap (org.webjars:bootstrap:5.3.8 - http://webjars.org)
@@ -480,8 +480,8 @@ conditions of the following licenses.
         * Jettison (org.codehaus.jettison:jettison:1.5.7 - 
https://github.com/jettison-json/jettison)
         * JODConverter Core (org.jodconverter:jodconverter-core:4.4.11 - 
https://github.com/jodconverter/jodconverter/)
         * JODConverter Local (org.jodconverter:jodconverter-local:4.4.11 - 
https://github.com/jodconverter/jodconverter/)
-        * jqplot4java (org.wicketstuff:jqplot4java:10.11.0 - 
http://maven.apache.org)
-        * JqPlot Plugin Integration (org.wicketstuff:jqplot:10.11.0 - 
http://wicketstuff.org/jqplot-parent/jqplot)
+        * jqplot4java (org.wicketstuff:jqplot4java:10.12.0-SNAPSHOT - 
http://maven.apache.org)
+        * JqPlot Plugin Integration (org.wicketstuff:jqplot:10.12.0-SNAPSHOT - 
http://wicketstuff.org/jqplot-parent/jqplot)
         * JSpecify annotations (org.jspecify:jspecify:1.0.0 - 
http://jspecify.org/)
         * jwarc (org.netpreserve:jwarc:0.37.0 - https://github.com/iipc/jwarc)
         * Kurento Client (org.kurento:kurento-client:7.3.0 - 
https://kurento.openvidu.io/)
@@ -551,23 +551,28 @@ conditions of the following licenses.
         * swagger-annotations (io.swagger.core.v3:swagger-annotations:2.2.55 - 
https://github.com/swagger-api/swagger-core/modules/swagger-annotations)
         * tomcat (org.apache.tomcat:tomcat:11.0.26 - 
https://tomcat.apache.org/)
         * UNO interface library (org.openoffice:unoil:4.1.2 - 
https://wiki.openoffice.org/wiki/Uno)
-        * Wicket Auth Roles (org.apache.wicket:wicket-auth-roles:10.11.0 - 
http://wicket.apache.org/wicket-auth-roles)
-        * Wicket Core (org.apache.wicket:wicket-core:10.11.0 - 
http://wicket.apache.org/wicket-core)
-        * Wicket Development Utilities 
(org.apache.wicket:wicket-devutils:10.11.0 - 
http://wicket.apache.org/wicket-devutils)
-        * Wicket Extensions (org.apache.wicket:wicket-extensions:10.11.0 - 
http://wicket.apache.org/wicket-extensions)
-        * Wicket IoC common code (org.apache.wicket:wicket-ioc:10.11.0 - 
http://wicket.apache.org/wicket-ioc)
+        * Wicket Auth Roles 
(org.apache.wicket:wicket-auth-roles:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-auth-roles)
+        * Wicket Core (org.apache.wicket:wicket-core:10.12.0-20260929.171021-8 
- http://wicket.apache.org/wicket-core)
+        * Wicket Core (org.apache.wicket:wicket-core:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-core)
+        * Wicket Development Utilities 
(org.apache.wicket:wicket-devutils:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-devutils)
+        * Wicket Extensions 
(org.apache.wicket:wicket-extensions:10.12.0-20260929.171021-6 - 
http://wicket.apache.org/wicket-extensions)
+        * Wicket Extensions 
(org.apache.wicket:wicket-extensions:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-extensions)
+        * Wicket IoC common code 
(org.apache.wicket:wicket-ioc:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-ioc)
         * Wicket JQuery Selectors 
(de.agilecoders.wicket:jquery-selectors:4.0.15 - 
http://nexus.sonatype.org/oss-repository-hosting.html/jquery-selectors)
-        * wicket-jquery-ui-calendar 
(org.wicketstuff:wicketstuff-jquery-ui-calendar:10.11.0 - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui-calendar)
-        * wicket-jquery-ui-core 
(org.wicketstuff:wicketstuff-jquery-ui-core:10.11.0 - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui-core)
-        * wicket-jquery-ui-plugins 
(org.wicketstuff:wicketstuff-jquery-ui-plugins:10.11.0 - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui-plugins)
-        * Wicket Native WebSocket Core 
(org.apache.wicket:wicket-native-websocket-core:10.11.0 - 
http://wicket.apache.org/wicket-native-websocket/wicket-native-websocket-core)
-        * Wicket Native WebSocket Javax 
(org.apache.wicket:wicket-native-websocket-javax:10.11.0 - 
http://wicket.apache.org/wicket-native-websocket/wicket-native-websocket-javax)
-        * Wicket Request (org.apache.wicket:wicket-request:10.11.0 - 
http://maven.apache.org)
-        * Wicket Spring Integration (org.apache.wicket:wicket-spring:10.11.0 - 
http://wicket.apache.org/wicket-spring)
-        * Wicketstuff Dashboard Core 
(org.wicketstuff:wicketstuff-dashboard-core:10.11.0 - 
http://wicketstuff.org/wicketstuff-dashboard-parent/wicketstuff-dashboard-core)
-        * wicketstuff-jquery-ui (org.wicketstuff:wicketstuff-jquery-ui:10.11.0 
- https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui)
-        * Wicketstuff-Select2 Components 
(org.wicketstuff:wicketstuff-select2:10.11.0 - 
http://wicketstuff.org/wicketstuff-select2-parent/wicketstuff-select2)
-        * Wicket Util (org.apache.wicket:wicket-util:10.11.0 - 
http://wicket.apache.org/wicket-util)
+        * wicket-jquery-ui-calendar 
(org.wicketstuff:wicketstuff-jquery-ui-calendar:10.12.0-SNAPSHOT - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui-calendar)
+        * wicket-jquery-ui-core 
(org.wicketstuff:wicketstuff-jquery-ui-core:10.12.0-SNAPSHOT - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui-core)
+        * wicket-jquery-ui-plugins 
(org.wicketstuff:wicketstuff-jquery-ui-plugins:10.12.0-SNAPSHOT - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui-plugins)
+        * Wicket Native WebSocket Core 
(org.apache.wicket:wicket-native-websocket-core:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-native-websocket/wicket-native-websocket-core)
+        * Wicket Native WebSocket Javax 
(org.apache.wicket:wicket-native-websocket-javax:10.12.0-20260929.171021-6 - 
http://wicket.apache.org/wicket-native-websocket/wicket-native-websocket-javax)
+        * Wicket Native WebSocket Javax 
(org.apache.wicket:wicket-native-websocket-javax:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-native-websocket/wicket-native-websocket-javax)
+        * Wicket Request 
(org.apache.wicket:wicket-request:10.12.0-20260929.171021-8 - 
http://maven.apache.org)
+        * Wicket Request (org.apache.wicket:wicket-request:10.12.0-SNAPSHOT - 
http://maven.apache.org)
+        * Wicket Spring Integration 
(org.apache.wicket:wicket-spring:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-spring)
+        * Wicketstuff Dashboard Core 
(org.wicketstuff:wicketstuff-dashboard-core:10.12.0-SNAPSHOT - 
http://wicketstuff.org/wicketstuff-dashboard-parent/wicketstuff-dashboard-core)
+        * wicketstuff-jquery-ui 
(org.wicketstuff:wicketstuff-jquery-ui:10.12.0-SNAPSHOT - 
https://github.com/sebfz1/wicket-jquery-ui/wicketstuff-jquery-ui)
+        * Wicketstuff-Select2 Components 
(org.wicketstuff:wicketstuff-select2:10.12.0-SNAPSHOT - 
http://wicketstuff.org/wicketstuff-select2-parent/wicketstuff-select2)
+        * Wicket Util (org.apache.wicket:wicket-util:10.12.0-20260929.171021-8 
- http://wicket.apache.org/wicket-util)
+        * Wicket Util (org.apache.wicket:wicket-util:10.12.0-SNAPSHOT - 
http://wicket.apache.org/wicket-util)
         * Woodstox (com.fasterxml.woodstox:woodstox-core:7.2.1 - 
https://github.com/FasterXML/woodstox)
         * XmlBeans (org.apache.xmlbeans:xmlbeans:5.3.0 - 
https://xmlbeans.apache.org/)
         * XML Commons Resolver Component (xml-resolver:xml-resolver:1.2 - 
http://xml.apache.org/commons/components/resolver/)
diff --git 
a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
 
b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
index 9121b31ca..5839bbbc4 100644
--- 
a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
+++ 
b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/user/User.java
@@ -205,13 +205,13 @@ public class User extends HistoricalEntity {
                        return groupAdminAllowed;
                }
 
-               public static List<Right> getAllowed(boolean groupAdmin) {
+               public static Stream<Right> getAllowed(boolean groupAdmin) {
                        Stream<Right> stream = Stream.of(Right.values())
                                        .filter(r -> Right.GROUP_ADMIN != r);
                        if (groupAdmin) {
                                stream = 
stream.filter(Right::isGroupAdminAllowed);
                        }
-                       return stream.toList();
+                       return stream;
                }
        }
 
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/extra/ExtraForm.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/extra/ExtraForm.java
index 01b82539c..9239e7179 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/extra/ExtraForm.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/extra/ExtraForm.java
@@ -30,6 +30,7 @@ import org.apache.openmeetings.db.entity.user.Group;
 import org.apache.openmeetings.web.admin.AdminBaseForm;
 import org.apache.openmeetings.web.app.WebSession;
 import org.apache.openmeetings.web.common.GroupChoiceProvider;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.form.Form;
@@ -38,14 +39,13 @@ import org.apache.wicket.markup.html.form.TextArea;
 import org.apache.wicket.model.CompoundPropertyModel;
 import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.util.CollectionModel;
-import org.wicketstuff.select2.Select2MultiChoice;
 
 import jakarta.inject.Inject;
 
 public class ExtraForm extends AdminBaseForm<ExtraMenu> {
        private static final long serialVersionUID = 1L;
        private final WebMarkupContainer list;
-       final Select2MultiChoice<Group> groups = new 
Select2MultiChoice<>("groups"
+       final OmSelect2MultiChoice<Group> groups = new 
OmSelect2MultiChoice<>("groups"
                        , new CollectionModel<>(new ArrayList<>())
                        , new GroupChoiceProvider());
 
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupForm.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupForm.java
index 59eab4b9f..490f5ea3d 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupForm.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/groups/GroupForm.java
@@ -39,6 +39,7 @@ import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.web.admin.AdminBaseForm;
 import org.apache.openmeetings.web.admin.AdminUserChoiceProvider;
 import org.apache.openmeetings.web.common.UploadableImagePanel;
+import org.apache.openmeetings.web.util.OmSelect2Choice;
 import org.apache.openmeetings.util.StoredFile;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
@@ -52,7 +53,6 @@ import org.apache.wicket.markup.html.form.TextField;
 import org.apache.wicket.model.CompoundPropertyModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.ResourceModel;
-import org.wicketstuff.select2.Select2Choice;
 
 import jakarta.inject.Inject;
 
@@ -60,7 +60,7 @@ public class GroupForm extends AdminBaseForm<Group> {
        private static final long serialVersionUID = 1L;
        private GroupUsersPanel usersPanel;
        private final WebMarkupContainer groupList;
-       private final Select2Choice<User> userToadd;
+       private OmSelect2Choice<User> userToadd;
        private final NumberTextField<Integer> maxFilesSize = new 
NumberTextField<>("maxFilesSize");
        private final NumberTextField<Integer> maxRecordingsSize = new 
NumberTextField<>("maxRecordingsSize");
        private final NumberTextField<Integer> maxRooms = new 
NumberTextField<>("maxRooms");
@@ -107,11 +107,19 @@ public class GroupForm extends AdminBaseForm<Group> {
                setMultiPart(true);
                this.groupList = groupList;
                setOutputMarkupId(true);
+       }
+
+       static String formatUser(User choice) {
+               return String.format("%s [%s]", choice.getLogin(), 
choice.getDisplayName());
+       }
 
+       @Override
+       protected void onInitialize() {
+               super.onInitialize();
                usersPanel = new GroupUsersPanel("users", getGroupId());
                add(usersPanel);
 
-               add(userToadd = new Select2Choice<>("user2add", 
Model.of((User)null), new AdminUserChoiceProvider() {
+               add(userToadd = new OmSelect2Choice<>("user2add", 
Model.of((User)null), new AdminUserChoiceProvider() {
                        private static final long serialVersionUID = 1L;
 
                        @Override
@@ -142,15 +150,6 @@ public class GroupForm extends AdminBaseForm<Group> {
                                }
                        }
                }));
-       }
-
-       static String formatUser(User choice) {
-               return String.format("%s [%s]", choice.getLogin(), 
choice.getDisplayName());
-       }
-
-       @Override
-       protected void onInitialize() {
-               super.onInitialize();
                final boolean isGroupAdmin = hasGroupAdminLevel(getRights());
                setNewVisible(!isGroupAdmin);
                setNewRecordVisible(!isGroupAdmin);
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
index 60ce63657..592e0b5aa 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
@@ -50,6 +50,8 @@ import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.web.admin.AdminBaseForm;
 import org.apache.openmeetings.web.admin.AdminUserChoiceProvider;
 import org.apache.openmeetings.web.app.ClientManager;
+import org.apache.openmeetings.web.util.OmSelect2Choice;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
 import org.apache.openmeetings.web.util.RestrictiveChoiceProvider;
 import org.apache.openmeetings.web.util.RoomTypeDropDown;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -75,8 +77,6 @@ import org.apache.wicket.model.util.CollectionModel;
 import org.apache.wicket.util.string.Strings;
 import org.wicketstuff.select2.ChoiceProvider;
 import org.wicketstuff.select2.Response;
-import org.wicketstuff.select2.Select2Choice;
-import org.wicketstuff.select2.Select2MultiChoice;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.badge.BootstrapBadge;
 import 
de.agilecoders.wicket.core.markup.html.bootstrap.button.BootstrapAjaxButton;
@@ -168,7 +168,7 @@ public class RoomForm extends AdminBaseForm<Room> {
                for (Group org : orgList) {
                        orgRooms.add(new RoomGroup(org, getModelObject()));
                }
-               add(new Select2MultiChoice<>("groups", null, new 
RestrictiveChoiceProvider<RoomGroup>() {
+               add(new OmSelect2MultiChoice<>("groups", null, new 
RestrictiveChoiceProvider<RoomGroup>() {
                        private static final long serialVersionUID = 1L;
 
                        @Override
@@ -215,7 +215,7 @@ public class RoomForm extends AdminBaseForm<Room> {
                add(new 
CheckBox("allowRecording").setEnabled(isRecordingsEnabled()));
                add(new CheckBox("chatModerated"));
 
-               add(new Select2MultiChoice<>("hiddenElements", null, new 
ChoiceProvider<RoomElement>() {
+               add(new OmSelect2MultiChoice<>("hiddenElements", null, new 
ChoiceProvider<RoomElement>() {
                        private static final long serialVersionUID = 1L;
 
                        @Override
@@ -250,7 +250,7 @@ public class RoomForm extends AdminBaseForm<Room> {
                add(new CheckBox("filesOpened"));
 
                // Moderators
-               final Select2Choice<User> moderatorChoice = new 
Select2Choice<>("moderator2add", moderator2add, new AdminUserChoiceProvider() {
+               final OmSelect2Choice<User> moderatorChoice = new 
OmSelect2Choice<>("moderator2add", moderator2add, new AdminUserChoiceProvider() 
{
                        private static final long serialVersionUID = 1L;
 
                        @Override
@@ -322,7 +322,7 @@ public class RoomForm extends AdminBaseForm<Room> {
 
                // Files
                Form<Void> filesForm = new Form<>("filesForm");
-               add(filesForm.add(new Select2MultiChoice<>("files2add", 
files2add, new ChoiceProvider<BaseFileItem>() {
+               add(filesForm.add(new OmSelect2MultiChoice<>("files2add", 
files2add, new ChoiceProvider<BaseFileItem>() {
                                private static final long serialVersionUID = 1L;
 
                                @Override
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
index be1278515..368363df4 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/users/UserForm.java
@@ -23,11 +23,11 @@ import static 
org.apache.openmeetings.db.dao.user.UserDao.getNewUserInstance;
 import static org.apache.openmeetings.db.entity.user.User.DISPLAY_NAME_NA;
 import static org.apache.openmeetings.db.util.AuthLevelUtil.hasAdminLevel;
 import static org.apache.openmeetings.db.util.AuthLevelUtil.hasGroupAdminLevel;
-import static org.apache.openmeetings.web.app.WebSession.getRights;
-import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import static 
org.apache.openmeetings.util.OpenmeetingsVariables.getMinLoginLength;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.isOtpEnabled;
 import static 
org.apache.openmeetings.util.OpenmeetingsVariables.isSendRegisterEmail;
+import static org.apache.openmeetings.web.app.WebSession.getRights;
+import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import static 
org.apache.wicket.validation.validator.StringValidator.minimumLength;
 
 import java.util.ArrayList;
@@ -35,6 +35,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 import org.apache.openmeetings.core.util.StrongPasswordValidator;
 import org.apache.openmeetings.db.dao.server.LdapConfigDao;
@@ -48,10 +50,12 @@ import org.apache.openmeetings.db.entity.user.User.Type;
 import org.apache.openmeetings.db.util.AuthLevelUtil;
 import org.apache.openmeetings.service.mail.EmailManager;
 import org.apache.openmeetings.web.admin.AdminBaseForm;
+import org.apache.openmeetings.web.app.WebSession;
 import org.apache.openmeetings.web.common.CommunityUserForm;
 import org.apache.openmeetings.web.common.GeneralUserForm;
 import org.apache.openmeetings.web.common.UploadableProfileImagePanel;
 import org.apache.openmeetings.web.util.DateLabel;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
 import org.apache.openmeetings.web.util.RestrictiveChoiceProvider;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.form.OnChangeAjaxBehavior;
@@ -73,7 +77,6 @@ import 
org.danekja.java.util.function.serializable.SerializableConsumer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.wicketstuff.select2.Response;
-import org.wicketstuff.select2.Select2MultiChoice;
 
 import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
 import jakarta.inject.Inject;
@@ -91,6 +94,7 @@ public class UserForm extends AdminBaseForm<User> {
        private final WebMarkupContainer listContainer;
        private final WebMarkupContainer domain = new 
WebMarkupContainer("domain");
        private GeneralUserForm generalForm;
+       private OmSelect2MultiChoice<Right> rightsSelect;
        private final RequiredTextField<String> login = new 
RequiredTextField<>("login");
        private StrongPasswordValidator passValidator;
        private final PasswordTextField password = new 
PasswordTextField("password", new Model<>());
@@ -143,7 +147,7 @@ public class UserForm extends AdminBaseForm<User> {
                mainContainer.add(new DateLabel("inserted"));
                mainContainer.add(new DateLabel("updated"));
 
-               mainContainer.add(new Select2MultiChoice<>("rights", null, new 
RestrictiveChoiceProvider<Right>() {
+               rightsSelect = new OmSelect2MultiChoice<>("rights", null, new 
RestrictiveChoiceProvider<Right>() {
                        private static final long serialVersionUID = 1L;
 
                        @Override
@@ -156,21 +160,37 @@ public class UserForm extends AdminBaseForm<User> {
                                return choice.name();
                        }
 
+                       public String getDisabledAttribute() {
+                               return "data-disabled";
+                       }
+
+                       @Override
+                       public boolean isDisabled(Right choice) {
+                               if (hasAdminLevel(getRights())) {
+                                       return false;
+                               }
+                               return choice == null || 
!choice.isGroupAdminAllowed();
+                       }
+
                        @Override
                        public void query(String term, int page, 
Response<Right> response) {
                                boolean isGroupAdmin = 
hasGroupAdminLevel(getRights());
-                               for (Right r : Right.getAllowed(isGroupAdmin)) {
-                                       if (Strings.isEmpty(term) || 
r.name().contains(term)) {
-                                               response.add(r);
-                                       }
-                               }
+                               Right.getAllowed(isGroupAdmin)
+                                       .filter(r -> Strings.isEmpty(term) || 
r.name().contains(term))
+                                       .forEach(r -> response.add(r));
                        }
 
                        @Override
                        public Right fromId(String id) {
                                return Right.valueOf(id);
                        }
-               }));
+               });
+               mainContainer.add(rightsSelect.setLabel(new 
ResourceModel("881")));
+               rightsSelect.getSettings()
+                               
.setTemplateSelection("templateSelectionWithDisabled")
+                               .setTemplateResult("templateResultWithDisabled")
+                               .setDataAdapter("OmWithDisabledAdapter");
+
                mainContainer.add(new CommunityUserForm("comunity", 
getModel()));
                remove(validationBehavior);
                setNewRecordVisible(true);
@@ -186,15 +206,24 @@ public class UserForm extends AdminBaseForm<User> {
        protected void onModelChanged() {
                super.onModelChanged();
                User u = getModelObject();
-               boolean nd = !u.isDeleted();
                boolean isNew = u.getId() == null;
-               mainContainer.setEnabled(nd);
+               // This form is enabled if:
+               boolean nd = !u.isDeleted(); // user is NOT deleted
+               boolean enabled = nd
+                       && (isAdmin(getRights()) // edited by 'full' admin
+                               || isNew // the user is new
+                               // group admin can edit themselves
+                               || WebSession.getUserId().equals(u.getId())
+                               // the user is NOT new and has only rights 
allowed for group admin
+                               || (!isNew && 
u.getRights().stream().allMatch(Right::isGroupAdminAllowed))
+                       );
+               mainContainer.setEnabled(enabled);
                otpEnabled.setModelObject(u.getOtpSecret() != null)
                                .setEnabled(isOtpEnabled() && u.getOtpSecret() 
!= null); // admin can only disable OTP
-               setSaveVisible(nd);
-               setDelVisible(nd && !isNew);
-               setRestoreVisible(!nd);
-               setPurgeVisible(!isNew);
+               setSaveVisible(enabled);
+               setDelVisible(enabled && !isNew);
+               setRestoreVisible(enabled && !nd);
+               setPurgeVisible(enabled && !isNew);
                password.setModelObject(null);
                generalForm.updateModelObject(u, true);
                passValidator.setUser(u);
@@ -238,14 +267,14 @@ public class UserForm extends AdminBaseForm<User> {
                }
        }
 
-       private static boolean checkLevel(Set<User.Right> rights) {
+       private static boolean isAdmin(Set<User.Right> rights) {
                return hasAdminLevel(rights) || 
AuthLevelUtil.hasWebServiceLevel(rights);
        }
 
        boolean isAdminPassRequired() {
                User u = getModelObject();
                User ou = userDao.get(u.getId());
-               return checkLevel(u.getRights()) || (ou != null && 
checkLevel(ou.getRights()));
+               return isAdmin(u.getRights()) || (ou != null && 
isAdmin(ou.getRights()));
        }
 
        private void purgeUser(AjaxRequestTarget target) {
@@ -361,6 +390,17 @@ public class UserForm extends AdminBaseForm<User> {
                if (!userDao.checkLogin(login.getConvertedInput(), u.getType(), 
u.getDomainId(), u.getId())) {
                        error(getString("error.login.inuse"));
                }
+               if (u.getId() != null && !isAdmin(getRights())) {
+                       // group admin, let's check rights modifications
+                       Set<Right> originalRights = 
userDao.get(u.getId()).getRights();
+                       Set<Right> currentRights = 
Set.copyOf(rightsSelect.getConvertedInput());
+                       if 
(Stream.concat(originalRights.stream().filter(Predicate.not(currentRights::contains))
+                                       , 
currentRights.stream().filter(Predicate.not(originalRights::contains)))
+                               
.anyMatch(Predicate.not(Right::isGroupAdminAllowed)))
+                       {
+                               rightsSelect.error(getString("204") + " " + 
getString("881"));
+                       }
+               }
                super.onValidate();
        }
 
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/WebSession.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/WebSession.java
index e218ceb45..583c2ac5e 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/WebSession.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/WebSession.java
@@ -420,6 +420,10 @@ public class WebSession extends 
AbstractAuthenticatedWebSession implements IWebS
                return session.languageId;
        }
 
+       public static boolean isRtl() {
+               return LabelDao.getLanguage(getLanguage()).isRtl();
+       }
+
        public static Long getUserId() {
                checkIsInvalid();
                return get().userId;
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/GeneralUserForm.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/GeneralUserForm.java
index e8a9a4df0..c31122168 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/GeneralUserForm.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/GeneralUserForm.java
@@ -18,14 +18,19 @@
  */
 package org.apache.openmeetings.web.common;
 
+import static org.apache.openmeetings.db.util.AuthLevelUtil.hasAdminLevel;
 import static org.apache.openmeetings.db.util.AuthLevelUtil.hasGroupAdminLevel;
+import static 
org.apache.openmeetings.util.OpenmeetingsVariables.isDisplayNameEditable;
 import static org.apache.openmeetings.web.app.WebSession.AVAILABLE_TIMEZONES;
 import static org.apache.openmeetings.web.app.WebSession.getRights;
 import static org.apache.openmeetings.web.app.WebSession.getUserId;
-import static 
org.apache.openmeetings.util.OpenmeetingsVariables.isDisplayNameEditable;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.openmeetings.db.dao.user.GroupDao;
 import org.apache.openmeetings.db.dao.user.UserDao;
@@ -33,10 +38,15 @@ import org.apache.openmeetings.db.entity.user.Group;
 import org.apache.openmeetings.db.entity.user.GroupUser;
 import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.entity.user.User.Salutation;
+import org.apache.openmeetings.web.app.WebSession;
 import org.apache.openmeetings.web.common.datetime.AjaxOmDatePicker;
 import org.apache.openmeetings.web.util.CountryDropDown;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
 import org.apache.openmeetings.web.util.RestrictiveChoiceProvider;
 import 
org.apache.wicket.extensions.validation.validator.RfcCompliantEmailAddressValidator;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.head.PriorityHeaderItem;
 import org.apache.wicket.markup.html.form.DropDownChoice;
 import org.apache.wicket.markup.html.form.Form;
 import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
@@ -47,9 +57,9 @@ import 
org.apache.wicket.markup.html.panel.IMarkupSourcingStrategy;
 import org.apache.wicket.markup.html.panel.PanelMarkupSourcingStrategy;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
 import org.apache.wicket.util.string.Strings;
 import org.wicketstuff.select2.Response;
-import org.wicketstuff.select2.Select2MultiChoice;
 
 import jakarta.inject.Inject;
 
@@ -58,7 +68,54 @@ public class GeneralUserForm extends Form<User> {
        private final RequiredTextField<String> email = new 
RequiredTextField<>("address.email");
        private final List<GroupUser> grpUsers = new ArrayList<>();
        private final AjaxOmDatePicker bday = new AjaxOmDatePicker("age");
+       private final OmSelect2MultiChoice<GroupUser> groupSelect = new 
OmSelect2MultiChoice<>("groupUsers", null, new 
RestrictiveChoiceProvider<GroupUser>() {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               public String getDisplayValue(GroupUser choice) {
+                       return choice.getGroup().getName();
+               }
+
+               @Override
+               public String toId(GroupUser choice) {
+                       Long id = choice.getGroup().getId();
+                       return id == null ? null : "" + id;
+               }
+
+               public String getDisabledAttribute() {
+                       return "data-disabled";
+               }
+
+               @Override
+               public boolean isDisabled(GroupUser choice) {
+                       if (hasAdminLevel(getRights())) {
+                               return false;
+                       }
+                       Long id = choice.getGroup().getId();
+                       return id != null && !adminGrps.contains(id);
+               }
+
+               @Override
+               public void query(String term, int page, Response<GroupUser> 
response) {
+                       for (GroupUser ou : grpUsers) {
+                               if (Strings.isEmpty(term) || 
ou.getGroup().getName().contains(term)) {
+                                       response.add(ou);
+                               }
+                       }
+               }
+
+               @Override
+               public GroupUser fromId(String inId) {
+                       Long id = Long.parseLong(inId);
+                       User u = GeneralUserForm.this.getModelObject();
+                       Group g = groupDao.get(id);
+                       GroupUser gu = new GroupUser(g, u);
+                       int idx = grpUsers.indexOf(gu);
+                       return idx < 0 ? gu : grpUsers.get(idx);
+               }
+       });
        private final boolean isAdminForm;
+       private final Set<Long> adminGrps;
 
        @Inject
        private GroupDao groupDao;
@@ -70,6 +127,11 @@ public class GeneralUserForm extends Form<User> {
                this.isAdminForm = isAdminForm;
                updateModelObject(getModelObject(), isAdminForm);
                setOutputMarkupId(true);
+               adminGrps = 
userDao.get(WebSession.getUserId()).getGroupUsers().stream()
+                               .filter(GroupUser::isModerator)
+                               .map(GroupUser::getGroup)
+                               .map(Group::getId)
+                               .collect(Collectors.toSet());
        }
 
        @Override
@@ -94,39 +156,21 @@ public class GeneralUserForm extends Form<User> {
                add(new TextField<String>("address.town"));
                add(new CountryDropDown("address.country"));
                add(new TextArea<String>("address.comment"));
-               add(new Select2MultiChoice<>("groupUsers", null, new 
RestrictiveChoiceProvider<GroupUser>() {
-                       private static final long serialVersionUID = 1L;
-
-                       @Override
-                       public String getDisplayValue(GroupUser choice) {
-                               return choice.getGroup().getName();
-                       }
+               groupSelect.getSettings()
+                               
.setTemplateSelection("templateSelectionWithDisabled")
+                               .setTemplateResult("templateResultWithDisabled")
+                               .setDataAdapter("OmWithDisabledAdapter");
+               add(groupSelect
+                               .setLabel(new ResourceModel("161"))
+                               .setRequired(isAdminForm && 
hasGroupAdminLevel(getRights()))
+                               .setEnabled(isAdminForm));
+       }
 
-                       @Override
-                       public String toId(GroupUser choice) {
-                               Long id = choice.getGroup().getId();
-                               return id == null ? null : "" + id;
-                       }
 
-                       @Override
-                       public void query(String term, int page, 
Response<GroupUser> response) {
-                               for (GroupUser ou : grpUsers) {
-                                       if (Strings.isEmpty(term) || 
ou.getGroup().getName().contains(term)) {
-                                               response.add(ou);
-                                       }
-                               }
-                       }
-
-                       @Override
-                       public GroupUser fromId(String inId) {
-                               Long id = Long.parseLong(inId);
-                               User u = GeneralUserForm.this.getModelObject();
-                               Group g = groupDao.get(id);
-                               GroupUser gu = new GroupUser(g, u);
-                               int idx = grpUsers.indexOf(gu);
-                               return idx < 0 ? gu : grpUsers.get(idx);
-                       }
-               }).setLabel(new ResourceModel("161")).setRequired(isAdminForm 
&& hasGroupAdminLevel(getRights())).setEnabled(isAdminForm));
+       @Override
+       public void renderHead(IHeaderResponse response) {
+               super.renderHead(response);
+               response.render(new 
PriorityHeaderItem(JavaScriptHeaderItem.forReference(new 
JavaScriptResourceReference(getClass(), "general-user-form.js"), 
"general-user-form")));
        }
 
        public void updateModelObject(User u, boolean isAdminForm) {
@@ -152,6 +196,23 @@ public class GeneralUserForm extends Form<User> {
                if (!userDao.checkEmail(email.getConvertedInput(), u.getType(), 
u.getDomainId(), u.getId())) {
                        error(getString("error.email.inuse"));
                }
+               if (u.getId() != null && isAdminForm && 
!hasAdminLevel(getRights())) {
+                       // group admin, let's check group modifications
+                       Set<Long> originalGroups = 
userDao.get(u.getId()).getGroupUsers().stream()
+                               .map(GroupUser::getGroup)
+                               .map(Group::getId)
+                               .collect(Collectors.toSet());
+                       Set<Long> currentGroups = 
groupSelect.getConvertedInput().stream()
+                               .map(GroupUser::getGroup)
+                               .map(Group::getId)
+                               .collect(Collectors.toSet());
+                       if 
(Stream.concat(originalGroups.stream().filter(Predicate.not(currentGroups::contains))
+                                       , 
currentGroups.stream().filter(Predicate.not(originalGroups::contains)))
+                               .anyMatch(Predicate.not(adminGrps::contains)))
+                       {
+                               groupSelect.error(getString("204") + " " + 
getString("161"));
+                       }
+               }
                super.onValidate();
        }
 
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/general-user-form.js
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/general-user-form.js
new file mode 100644
index 000000000..5fd804c56
--- /dev/null
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/common/general-user-form.js
@@ -0,0 +1,32 @@
+/* Licensed under the Apache License, Version 2.0 (the "License") 
http://www.apache.org/licenses/LICENSE-2.0 */
+const AjaxAdapter = $.fn.select2.amd.require('select2/data/ajax');
+class OmWithDisabledAdapter extends AjaxAdapter {
+       constructor($element, options) {
+               super($element, options);
+       }
+
+       item($element) {
+               const res = super.item($element);
+               if ($element.data('disabled') === 'data-disabled') {
+                       res.disabled = true;
+               }
+               return res;
+       }
+}
+
+function templateSelectionWithDisabled(data, container) {
+       if (data.disabled === true) {
+               container.addClass('select2-results__option--disabled');
+               container.find('button').css('pointer-events', 'none');
+       }
+       return data.text;
+}
+
+function templateResultWithDisabled(data, container) {
+       if (data.disabled === true && data.loading !== true) {
+               
container.classList.remove('select2-results__option--selectable');
+               container.classList.add('select2-results__option--disabled');
+               container.style.pointerEvents = 'none';
+       }
+       return data.text;
+}
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomInvitationForm.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomInvitationForm.java
index 63dc55f34..2fbd55417 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomInvitationForm.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomInvitationForm.java
@@ -35,6 +35,7 @@ import org.apache.openmeetings.service.room.InvitationManager;
 import org.apache.openmeetings.web.app.WebSession;
 import org.apache.openmeetings.web.common.GroupChoiceProvider;
 import org.apache.openmeetings.web.common.InvitationForm;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
 import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
@@ -49,7 +50,6 @@ import org.apache.wicket.model.util.CollectionModel;
 import org.apache.wicket.util.string.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.wicketstuff.select2.Select2MultiChoice;
 
 import jakarta.inject.Inject;
 
@@ -59,7 +59,7 @@ public class RoomInvitationForm extends InvitationForm {
        private final RadioGroup<InviteeType> rdi = new 
RadioGroup<>("inviteeType", Model.of(InviteeType.USER));
        private final Long roomId;
        private final WebMarkupContainer groupContainer = new 
WebMarkupContainer("groupContainer");
-       private final Select2MultiChoice<Group> groups = new 
Select2MultiChoice<>("groups"
+       private final OmSelect2MultiChoice<Group> groups = new 
OmSelect2MultiChoice<>("groups"
                        , new CollectionModel<>(new ArrayList<>())
                        , new GroupChoiceProvider());
        final WebMarkupContainer sipContainer = new 
WebMarkupContainer("sip-container");
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/calendar/AppointmentDialog.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/calendar/AppointmentDialog.java
index c9087e4ac..183e4d966 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/calendar/AppointmentDialog.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/calendar/AppointmentDialog.java
@@ -56,6 +56,7 @@ import 
org.apache.openmeetings.web.common.datetime.OmDateTimePicker;
 import org.apache.openmeetings.web.pages.MainPage;
 import org.apache.openmeetings.web.user.OmWysiwygToolbar;
 import org.apache.openmeetings.web.user.rooms.RoomEnterBehavior;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
 import org.apache.openmeetings.web.util.RoomTypeDropDown;
 import org.apache.openmeetings.web.util.UserMultiChoice;
 import org.apache.wicket.AttributeModifier;
@@ -86,7 +87,6 @@ import org.apache.wicket.model.util.CollectionModel;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.wicketstuff.select2.Select2MultiChoice;
 
 import org.wicketstuff.jquery.ui.plugins.wysiwyg.WysiwygEditor;
 
@@ -109,7 +109,7 @@ public class AppointmentDialog extends Modal<Appointment> {
        private final NotificationPanel feedback = new 
NotificationPanel("feedback");
        private final WebMarkupContainer sipContainer = new 
WebMarkupContainer("sip-container");
        private final RadioGroup<InviteeType> rdi = new 
RadioGroup<>("inviteeType", Model.of(InviteeType.USER));
-       private final Select2MultiChoice<Group> groups = new 
Select2MultiChoice<>("groups"
+       private final OmSelect2MultiChoice<Group> groups = new 
OmSelect2MultiChoice<>("groups"
                        , new CollectionModel<>(new ArrayList<>())
                        , new GroupChoiceProvider());
        private final UserMultiChoice attendees = new 
UserMultiChoice("attendees", new CollectionModel<>(new ArrayList<>()));
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/OmSelect2Choice.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/OmSelect2Choice.java
new file mode 100644
index 000000000..07decc9ab
--- /dev/null
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/OmSelect2Choice.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.util;
+
+import org.apache.openmeetings.web.app.WebSession;
+import org.apache.wicket.model.IModel;
+import org.wicketstuff.select2.ChoiceProvider;
+import org.wicketstuff.select2.Select2Choice;
+import org.wicketstuff.select2.Settings;
+
+public class OmSelect2Choice<T> extends Select2Choice<T> {
+       public OmSelect2Choice(String id, IModel<T> model, ChoiceProvider<T> 
provider) {
+               super(id, model, provider);
+               getSettings()
+                       .setDir(WebSession.isRtl() ? Settings.DIRECTION.rtl : 
Settings.DIRECTION.ltr);
+       }
+}
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/OmSelect2MultiChoice.java
similarity index 50%
copy from 
openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
copy to 
openmeetings-web/src/main/java/org/apache/openmeetings/web/util/OmSelect2MultiChoice.java
index f56f6b46d..50013f44e 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/OmSelect2MultiChoice.java
@@ -20,27 +20,16 @@ package org.apache.openmeetings.web.util;
 
 import java.util.Collection;
 
-import org.apache.openmeetings.db.entity.user.User;
-import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.head.JavaScriptHeaderItem;
-import org.apache.wicket.markup.head.PriorityHeaderItem;
+import org.apache.openmeetings.web.app.WebSession;
 import org.apache.wicket.model.IModel;
-import org.apache.wicket.request.resource.JavaScriptResourceReference;
+import org.wicketstuff.select2.ChoiceProvider;
 import org.wicketstuff.select2.Select2MultiChoice;
+import org.wicketstuff.select2.Settings;
 
-public class UserMultiChoice extends Select2MultiChoice<User> {
-       private static final long serialVersionUID = 1L;
-
-       public UserMultiChoice(String id, IModel<Collection<User>> model) {
-               super(id, model, new UserChoiceProvider());
-               getSettings().setTemplateResult("formatOmUser");
-               getSettings().setTemplateSelection("formatOmUser");
-               getSettings().setEscapeMarkup("escapeOmUserMarkup");
-       }
-
-       @Override
-       public void renderHead(IHeaderResponse response) {
-               super.renderHead(response);
-               response.render(new 
PriorityHeaderItem(JavaScriptHeaderItem.forReference(new 
JavaScriptResourceReference(getClass(), "user-multi-choice.js"), 
"user-multi-choice")));
+public class OmSelect2MultiChoice<T> extends Select2MultiChoice<T> {
+       public OmSelect2MultiChoice(String id, IModel<Collection<T>> model, 
ChoiceProvider<T> provider) {
+               super(id, model, provider);
+               getSettings()
+                       .setDir(WebSession.isRtl() ? Settings.DIRECTION.rtl : 
Settings.DIRECTION.ltr);
        }
 }
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
index f56f6b46d..f4448ee65 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/UserMultiChoice.java
@@ -26,9 +26,8 @@ import org.apache.wicket.markup.head.JavaScriptHeaderItem;
 import org.apache.wicket.markup.head.PriorityHeaderItem;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.request.resource.JavaScriptResourceReference;
-import org.wicketstuff.select2.Select2MultiChoice;
 
-public class UserMultiChoice extends Select2MultiChoice<User> {
+public class UserMultiChoice extends OmSelect2MultiChoice<User> {
        private static final long serialVersionUID = 1L;
 
        public UserMultiChoice(String id, IModel<Collection<User>> model) {
diff --git 
a/openmeetings-web/src/test/java/org/apache/openmeetings/web/GroupAdminTest.java
 
b/openmeetings-web/src/test/java/org/apache/openmeetings/web/GroupAdminTest.java
new file mode 100644
index 000000000..78e8eaa42
--- /dev/null
+++ 
b/openmeetings-web/src/test/java/org/apache/openmeetings/web/GroupAdminTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web;
+
+import static java.util.UUID.randomUUID;
+
+import static org.apache.openmeetings.db.util.AuthLevelUtil.hasAdminLevel;
+import static org.apache.openmeetings.db.util.AuthLevelUtil.hasGroupAdminLevel;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.openmeetings.db.dao.room.RoomDao;
+import org.apache.openmeetings.db.entity.room.Room;
+import org.apache.openmeetings.db.entity.user.Group;
+import org.apache.openmeetings.db.entity.user.GroupUser;
+import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.db.manager.RoomManager;
+import org.apache.openmeetings.web.admin.users.UserForm;
+import org.apache.openmeetings.web.admin.users.UsersPanel;
+import org.apache.openmeetings.web.pages.MainPage;
+import org.apache.openmeetings.web.util.OmSelect2MultiChoice;
+import org.apache.openmeetings.web.util.OmUrlFragment.AreaKeys;
+import org.apache.wicket.behavior.AbstractAjaxBehavior;
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.protocol.ws.tester.WebSocketTester;
+import org.junit.jupiter.api.Test;
+
+import jakarta.inject.Inject;
+
+public class GroupAdminTest extends AbstractWicketTesterTest {
+       @Inject
+       private RoomDao roomDao;
+       @Inject
+       private RoomManager roomManager;
+
+       @Test
+       void groupAdminCanRemoveVictimsUnrelatedMembershipThroughUserForm() 
throws Exception {
+               String token = randomUUID().toString();
+               Group groupA = groupDao.update(new Group().setName("i007-A-" + 
token), null);
+               Group groupB = groupDao.update(new Group().setName("i007-B-" + 
token), null);
+
+               User attacker = createUser("i007-attacker-" + token);
+               attacker.setGroupUsers(new ArrayList<>());
+               GroupUser attackerMembership = new GroupUser(groupA, attacker);
+               attackerMembership.setModerator(true);
+               attacker.getGroupUsers().add(attackerMembership);
+               attacker = userDao.update(attacker, null);
+
+               User victim = createUser("i007-victim-" + token);
+               victim.setGroupUsers(new ArrayList<>());
+               victim.addGroup(groupA);
+               victim.addGroup(groupB);
+               victim = userDao.update(victim, null);
+
+               Room room = new Room();
+               room.setName("i007-B-only-" + token);
+               room.setType(Room.Type.CONFERENCE);
+               room.setIspublic(false);
+               room.setOwnerId(userDao.get(1L).getId());
+               room.addGroup(groupB);
+               room = roomDao.update(room, userDao.get(1L).getId());
+
+               User attackerReloaded = userDao.get(attacker.getId());
+               User victimBefore = userDao.get(victim.getId());
+               Room roomBefore = roomDao.get(room.getId());
+               assertNotNull(attackerReloaded);
+               assertNotNull(victimBefore);
+               assertNotNull(roomBefore);
+               assertTrue(hasGroupAdminLevel(attackerReloaded.getRights()) || 
attackerReloaded.getGroupUsers().stream().anyMatch(GroupUser::isModerator));
+               assertFalse(hasAdminLevel(attackerReloaded.getRights()), 
"attacker must not be global ADMIN");
+               
assertTrue(groupIds(victimBefore).containsAll(Set.of(groupA.getId(), 
groupB.getId())));
+               assertTrue(roomManager.isRoomAllowedToUser(roomBefore, 
victimBefore), "victim initially reaches B-only room");
+
+               login(attacker.getLogin(), createPass());
+               MainPage page = tester.startPage(MainPage.class);
+               tester.assertRenderedPage(MainPage.class);
+               
tester.executeBehavior((AbstractAjaxBehavior)page.getBehaviorById(1));
+               
tester.executeBehavior((AbstractAjaxBehavior)page.get("main-container").getBehaviorById(0));
+               WebSocketTester webSocketTester = new WebSocketTester(tester, 
page);
+               
webSocketTester.sendMessage(org.apache.openmeetings.web.common.OmWebSocketPanel.CONNECTED_MSG);
+               tester.getRequest().setParameter(AreaKeys.ADMIN.zone(), "user");
+               
tester.executeBehavior((AbstractAjaxBehavior)page.getBehaviorById(0));
+               tester.assertComponent(PATH_CHILD, UsersPanel.class);
+               UsersPanel usersPanel = (UsersPanel)page.get(PATH_CHILD);
+
+               Item<?> victimItem = null;
+               for (int i = 0; i < 20; ++i) {
+                       Component candidate = 
usersPanel.get("listContainer:userList:" + i);
+                       if (candidate != null && 
victim.getId().equals(candidate.getDefaultModelObject() instanceof User u ? 
u.getId() : null)) {
+                               victimItem = (Item<?>)candidate;
+                               break;
+                       }
+               }
+               assertNotNull(victimItem, "victim must be present in 
GROUP_ADMIN-scoped list");
+               
tester.executeBehavior((AbstractAjaxBehavior)victimItem.getBehaviorById(0));
+
+               UserForm form = (UserForm)usersPanel.get("form");
+               @SuppressWarnings("unchecked")
+               OmSelect2MultiChoice<GroupUser> memberships = 
(OmSelect2MultiChoice<GroupUser>)form.get("adminForm:general:groupUsers");
+               Collection<GroupUser> loadedMemberships = 
memberships.getModelObject();
+               assertNotNull(loadedMemberships);
+               
assertTrue(groupIds(loadedMemberships).containsAll(Set.of(groupA.getId(), 
groupB.getId())), "loaded form contains A and unrelated B");
+
+               memberships.setModelObject(loadedMemberships.stream().filter(gu 
-> groupA.getId().equals(gu.getGroup().getId())).toList());
+               
tester.executeBehavior((AbstractAjaxBehavior)form.get("buttons:btn-save").getBehaviorById(0));
+
+               User victimAfter = userDao.get(victim.getId());
+               Room roomAfter = roomDao.get(room.getId());
+               Set<Long> afterGroups = groupIds(victimAfter);
+               boolean accessAfter = 
roomManager.isRoomAllowedToUser(roomAfter, victimAfter);
+               assertTrue(afterGroups.contains(groupA.getId()));
+               assertFalse(afterGroups.contains(groupB.getId()), "B membership 
must be deleted by persisted form collection");
+               assertFalse(accessAfter, "victim must lose B-only room access 
after B membership deletion");
+       }
+
+       private static Set<Long> groupIds(User user) {
+               return groupIds(user.getGroupUsers());
+       }
+
+       private static Set<Long> groupIds(Collection<GroupUser> memberships) {
+               return memberships.stream().map(gu -> 
gu.getGroup().getId()).collect(Collectors.toSet());
+       }
+}
diff --git a/pom.xml b/pom.xml
index 1bb673fd9..9ba77ec12 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,8 +87,8 @@
 
                <!-- dependency versions -->
                <junit.version>6.1.3</junit.version>
-               <wicket.version>10.11.0</wicket.version>
-               <wicketstuff.version>10.11.0</wicketstuff.version>
+               <wicket.version>10.12.0-SNAPSHOT</wicket.version>
+               <wicketstuff.version>10.12.0-SNAPSHOT</wicketstuff.version>
                <wicket-bootstrap.version>7.0.15</wicket-bootstrap.version>
                <fullcalendar.version>7.1.0</fullcalendar.version>
                <spring.version>7.0.9</spring.version>

Reply via email to