diff --git a/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelectionAction.java b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelectionAction.java
new file mode 100644
index 000000000..b6676481c
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelectionAction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.admin.ui.usersgroupsroles;
+
+import org.libreccm.security.Group;
+
+import java.util.Set;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+public interface GroupSelectionAction {
+
+ void action(Set selectedGroup);
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelector.java b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelector.java
new file mode 100644
index 000000000..6ef405a80
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelector.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.admin.ui.usersgroupsroles;
+
+import com.vaadin.icons.VaadinIcons;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Window;
+import com.vaadin.ui.components.grid.HeaderCell;
+import com.vaadin.ui.components.grid.HeaderRow;
+import com.vaadin.ui.themes.ValoTheme;
+import org.libreccm.cdi.utils.CdiUtil;
+import org.libreccm.security.Group;
+
+import java.util.List;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+public class GroupSelector extends Window {
+
+ private static final long serialVersionUID = -6227551833159691370L;
+
+ private static final String COL_NAME = "groupname";
+
+ public GroupSelector(final String caption,
+ final String actionLabel,
+ final UsersGroupsRoles usersGroupsRoles,
+ final List excludedGroups,
+ final GroupSelectionAction action) {
+
+ addWidgets(caption, actionLabel, excludedGroups, action);
+ }
+
+ private void addWidgets(final String caption,
+ final String actionLabel,
+ final List excludedGroups,
+ final GroupSelectionAction action) {
+
+ setCaption(caption);
+
+ final CdiUtil cdiUtil = CdiUtil.createCdiUtil();
+
+ final Grid groupsGrid = new Grid<>();
+ groupsGrid
+ .addColumn(Group::getName)
+ .setId(COL_NAME)
+ .setCaption("Group");
+
+ groupsGrid.setSelectionMode(Grid.SelectionMode.MULTI);
+ groupsGrid.setWidth("100%");
+
+ final Button actionButton = new Button(actionLabel);
+ actionButton.addClickListener(event -> {
+ action.action(groupsGrid.getSelectedItems());
+ close();
+ });
+ actionButton.setIcon(VaadinIcons.PLUS_CIRCLE_O);
+ actionButton.setStyleName(ValoTheme.BUTTON_TINY);
+
+ final Button clearButton = new Button("Clear selection");
+ clearButton.addClickListener(event -> {
+ groupsGrid.getSelectionModel().deselectAll();
+ });
+ clearButton.setIcon(VaadinIcons.BACKSPACE);
+ clearButton.setStyleName(ValoTheme.BUTTON_TINY);
+
+ final HeaderRow actions = groupsGrid.prependHeaderRow();
+ final HeaderCell actionsCell = actions.getCell(COL_NAME);
+ actionsCell.setComponent(new HorizontalLayout(actionButton,
+ clearButton));
+
+ final GroupSelectorDataProvider dataProvider = cdiUtil
+ .findBean(GroupSelectorDataProvider.class);
+
+ dataProvider.setExcludedGroups(excludedGroups);
+
+ groupsGrid.setDataProvider(dataProvider);
+
+ setContent(groupsGrid);
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelectorDataProvider.java b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelectorDataProvider.java
new file mode 100644
index 000000000..f048e9339
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/GroupSelectorDataProvider.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.admin.ui.usersgroupsroles;
+
+import com.vaadin.cdi.ViewScoped;
+import com.vaadin.data.provider.AbstractDataProvider;
+import com.vaadin.data.provider.Query;
+import org.libreccm.security.Group;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Root;
+import javax.transaction.Transactional;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@ViewScoped
+public class GroupSelectorDataProvider extends AbstractDataProvider {
+
+ private static final long serialVersionUID = 2237927716392108777L;
+
+ @Inject
+ private EntityManager entityManager;
+
+ private String groupNameFilter;
+
+ private List excludedGroups;
+
+ @Override
+ public boolean isInMemory() {
+ return false;
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public int size(final Query query) {
+
+ final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+ final CriteriaQuery criteriaQuery = builder
+ .createQuery(Long.class);
+ final Root from = criteriaQuery.from(Group.class);
+ criteriaQuery.select(builder.count(from));
+ criteriaQuery.distinct(true);
+
+ if (groupNameFilter != null && !groupNameFilter.trim().isEmpty()) {
+ criteriaQuery
+ .where(builder.like(builder.lower(from.get("name")),
+ String.format("%s%%", groupNameFilter)));
+ }
+
+ if (excludedGroups != null && !excludedGroups.isEmpty()) {
+ criteriaQuery.where(builder.not(from.in(excludedGroups)));
+ }
+
+ return entityManager
+ .createQuery(criteriaQuery)
+ .getSingleResult()
+ .intValue();
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public Stream fetch(final Query query) {
+
+ final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+ final CriteriaQuery criteriaQuery = builder
+ .createQuery(Group.class);
+ final Root from = criteriaQuery.from(Group.class);
+ criteriaQuery.distinct(true);
+
+ if (groupNameFilter != null && !groupNameFilter.trim().isEmpty()) {
+ criteriaQuery
+ .where(builder.like(builder.lower(from.get("name")),
+ String.format("%s%%", groupNameFilter)));
+ }
+
+ if (excludedGroups != null && !excludedGroups.isEmpty()) {
+ criteriaQuery.where(builder.not(from.in(excludedGroups)));
+ }
+
+ return entityManager
+ .createQuery(criteriaQuery)
+ .setMaxResults(query.getLimit())
+ .setFirstResult(query.getOffset())
+ .getResultList()
+ .stream();
+ }
+
+ public void setGroupNameFilter(final String groupNameFilter) {
+ this.groupNameFilter = groupNameFilter;
+ }
+
+ public void setExcludedGroups(final List excludedGroups) {
+ this.excludedGroups = excludedGroups;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/UserGroupsController.java b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/UserGroupsController.java
new file mode 100644
index 000000000..efc8311eb
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/admin/ui/usersgroupsroles/UserGroupsController.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.admin.ui.usersgroupsroles;
+
+import org.libreccm.security.Group;
+import org.libreccm.security.GroupManager;
+import org.libreccm.security.GroupRepository;
+import org.libreccm.security.User;
+import org.libreccm.security.UserRepository;
+
+import java.util.Set;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.transaction.Transactional;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+class UserGroupsController {
+
+ @Inject
+ private GroupRepository groupRepo;
+
+ @Inject
+ private GroupManager groupManager;
+
+ @Inject
+ private UserRepository userRepo;
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ protected void addUserToGroups(final User user, final Set groups) {
+
+ groups.forEach(group -> addUserToGroup(user, group));
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ protected void addUserToGroup(final User user, final Group group) {
+
+ final Group theGroup = groupRepo
+ .findById(group.getPartyId())
+ .orElseThrow(() -> new IllegalArgumentException(String
+ .format("No Group with ID %d in the database.",
+ group.getPartyId())));
+
+ final User theUser = userRepo
+ .findById(user.getPartyId())
+ .orElseThrow(() -> new IllegalArgumentException(String
+ .format("No user with ID %d in the database. ",
+ user.getPartyId())));
+
+ groupManager.addMemberToGroup(theUser, theGroup);
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ protected void removeUserFromGroup(final User user, final Group group) {
+
+ final Group theGroup = groupRepo
+ .findById(group.getPartyId())
+ .orElseThrow(() -> new IllegalArgumentException(String
+ .format("No Group with ID %d in the database.",
+ group.getPartyId())));
+
+ final User theUser = userRepo
+ .findById(user.getPartyId())
+ .orElseThrow(() -> new IllegalArgumentException(String
+ .format("No user with ID %d in the database. ",
+ user.getPartyId())));
+
+ groupManager.removeMemberFromGroup(theUser, theGroup);
+ }
+
+}