diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml
index c53eb4b44b..db8b8d0091 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml
@@ -37,6 +37,14 @@
+
+
+
+
+
+
+
+
@@ -58,6 +66,9 @@
+
+
+
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index e1333e9280..00785cf710 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -49,6 +49,7 @@ public class RealmRepresentation {
protected RolesRepresentation roles;
protected List groups;
protected List defaultRoles;
+ protected List defaultGroups;
@Deprecated
protected Set requiredCredentials;
protected String passwordPolicy;
@@ -269,6 +270,14 @@ public class RealmRepresentation {
this.defaultRoles = defaultRoles;
}
+ public List getDefaultGroups() {
+ return defaultGroups;
+ }
+
+ public void setDefaultGroups(List defaultGroups) {
+ this.defaultGroups = defaultGroups;
+ }
+
public String getPrivateKey() {
return privateKey;
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index 8ac199cdc6..6dee1cbf78 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -47,6 +47,19 @@ public interface RealmResource {
@Produces(MediaType.APPLICATION_JSON)
public GroupRepresentation getGroupByPath(@PathParam("path") String path);
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("default-groups")
+ public List getDefaultGroups();
+
+ @PUT
+ @Path("default-groups/{groupId}")
+ public void addDefaultGroup(@PathParam("groupId") String groupId);
+
+ @DELETE
+ @Path("default-groups/{groupId}")
+ public void removeDefaultGroup(@PathParam("groupId") String groupId);
+
@Path("identity-provider")
IdentityProvidersResource identityProviders();
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 641f37e419..62f9162e2d 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -165,6 +165,12 @@ public interface RealmModel extends RoleContainerModel {
void updateDefaultRoles(String[] defaultRoles);
+ List getDefaultGroups();
+
+ void addDefaultGroup(GroupModel group);
+
+ void removeDefaultGroup(GroupModel group);
+
// Key is clientId
Map getClientNameMap();
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 389ec0aabc..8f5c8440b5 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -61,6 +61,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
// We are using names of defaultRoles (not ids)
private List defaultRoles = new ArrayList();
+ private List defaultGroups = new ArrayList();
private List requiredCredentials = new ArrayList();
private List userFederationProviders = new ArrayList();
@@ -629,6 +630,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+
+ public List getDefaultGroups() {
+ return defaultGroups;
+ }
+
+ public void setDefaultGroups(List defaultGroups) {
+ this.defaultGroups = defaultGroups;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index b1d71ec64a..723617b7a6 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -239,6 +239,14 @@ public class ModelToRepresentation {
roleStrings.addAll(defaultRoles);
rep.setDefaultRoles(roleStrings);
}
+ List defaultGroups = realm.getDefaultGroups();
+ if (!defaultGroups.isEmpty()) {
+ List groupPaths = new LinkedList<>();
+ for (GroupModel group : defaultGroups) {
+ groupPaths.add(ModelToRepresentation.buildGroupPath(group));
+ }
+ rep.setDefaultGroups(groupPaths);
+ }
List requiredCredentialModels = realm.getRequiredCredentials();
if (requiredCredentialModels.size() > 0) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index bb9dbc8d3d..dde4462b97 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -315,6 +315,13 @@ public class RepresentationToModel {
if (rep.getGroups() != null) {
importGroups(newRealm, rep);
+ if (rep.getDefaultGroups() != null) {
+ for (String path : rep.getDefaultGroups()) {
+ GroupModel found = KeycloakModelUtils.findGroupByPath(newRealm, path);
+ if (found == null) throw new RuntimeException("default group in realm rep doesn't exist: " + path);
+ newRealm.addDefaultGroup(found);
+ }
+ }
}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 68e62bfee3..02abe5e99f 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -480,6 +480,30 @@ public class RealmAdapter implements RealmModel {
return cacheSession.getRoleById(id, this);
}
+ @Override
+ public List getDefaultGroups() {
+ List defaultGroups = new LinkedList<>();
+ for (String id : cached.getDefaultGroups()) {
+ defaultGroups.add(cacheSession.getGroupById(id, this));
+ }
+ return defaultGroups;
+
+ }
+
+ @Override
+ public void addDefaultGroup(GroupModel group) {
+ getDelegateForUpdate();
+ updated.addDefaultGroup(group);
+
+ }
+
+ @Override
+ public void removeDefaultGroup(GroupModel group) {
+ getDelegateForUpdate();
+ updated.removeDefaultGroup(group);
+
+ }
+
@Override
public List getDefaultRoles() {
if (updated != null) return updated.getDefaultRoles();
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index 604d7fa825..4ae07fd4f1 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -107,6 +107,7 @@ public class CachedRealm implements Serializable {
protected Set adminEnabledEventOperations = new HashSet();
protected boolean adminEventsDetailsEnabled;
private List defaultRoles = new LinkedList();
+ private List defaultGroups = new LinkedList();
private Set groups = new HashSet();
private Map realmRoles = new HashMap();
private Map clients = new HashMap();
@@ -229,6 +230,10 @@ public class CachedRealm implements Serializable {
requiredActionProvidersByAlias.put(action.getAlias(), action);
}
+ for (GroupModel group : model.getDefaultGroups()) {
+ defaultGroups.add(group.getId());
+ }
+
browserFlow = model.getBrowserFlow();
registrationFlow = model.getRegistrationFlow();
directGrantFlow = model.getDirectGrantFlow();
@@ -516,4 +521,8 @@ public class CachedRealm implements Serializable {
public Set getGroups() {
return groups;
}
+
+ public List getDefaultGroups() {
+ return defaultGroups;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 61e4982e88..64650c9d1d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -71,6 +71,10 @@ public class JpaUserProvider implements UserProvider {
userModel.grantRole(application.getRole(r));
}
}
+
+ for (GroupModel g : realm.getDefaultGroups()) {
+ userModel.joinGroup(g);
+ }
}
for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
if (r.isEnabled() && r.isDefaultAction()) {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index eae002b545..1397caddba 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -647,6 +647,44 @@ public class RealmAdapter implements RealmModel {
em.flush();
}
+ @Override
+ public List getDefaultGroups() {
+ Collection entities = realm.getDefaultGroups();
+ List defaultGroups = new LinkedList<>();
+ for (GroupEntity entity : entities) {
+ defaultGroups.add(session.realms().getGroupById(entity.getId(), this));
+ }
+ return defaultGroups;
+ }
+
+ @Override
+ public void addDefaultGroup(GroupModel group) {
+ Collection entities = realm.getDefaultGroups();
+ for (GroupEntity entity : entities) {
+ if (entity.getId().equals(group.getId())) return;
+ }
+ GroupEntity groupEntity = GroupAdapter.toEntity(group, em);
+ realm.getDefaultGroups().add(groupEntity);
+ em.flush();
+
+ }
+
+ @Override
+ public void removeDefaultGroup(GroupModel group) {
+ GroupEntity found = null;
+ for (GroupEntity defaultGroup : realm.getDefaultGroups()) {
+ if (defaultGroup.getId().equals(group.getId())) {
+ found = defaultGroup;
+ break;
+ }
+ }
+ if (found != null) {
+ realm.getDefaultGroups().remove(found);
+ em.flush();
+ }
+
+ }
+
@Override
public Map getClientNameMap() {
Map map = new HashMap();
@@ -2002,6 +2040,7 @@ public class RealmAdapter implements RealmModel {
if (!groupEntity.getRealm().getId().equals(getId())) {
return false;
}
+ realm.getDefaultRoles().remove(groupEntity);
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
index d5851fd1b6..2326cad628 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
@@ -92,7 +92,7 @@ public class GroupEntity {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (o == null) return false;
GroupEntity that = (GroupEntity) o;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 40c2568a39..6492b812a1 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -143,6 +143,10 @@ public class RealmEntity {
@JoinTable(name="REALM_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
protected Collection defaultRoles = new ArrayList();
+ @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
+ @JoinTable(name="REALM_DEFAULT_GROUPS", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="GROUP_ID")})
+ protected Collection defaultGroups = new ArrayList<>();
+
@Column(name="EVENTS_ENABLED")
protected boolean eventsEnabled;
@Column(name="EVENTS_EXPIRATION")
@@ -426,6 +430,14 @@ public class RealmEntity {
this.defaultRoles = defaultRoles;
}
+ public Collection getDefaultGroups() {
+ return defaultGroups;
+ }
+
+ public void setDefaultGroups(Collection defaultGroups) {
+ this.defaultGroups = defaultGroups;
+ }
+
public String getPasswordPolicy() {
return passwordPolicy;
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 9b4f1f89d1..44758b7fb3 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -300,6 +300,9 @@ public class MongoUserProvider implements UserProvider {
userModel.grantRole(application.getRole(r));
}
}
+ for (GroupModel g : realm.getDefaultGroups()) {
+ userModel.joinGroup(g);
+ }
}
if (addDefaultRequiredActions) {
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 2fe85a11a9..8f8d43dd78 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -681,6 +681,9 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
@Override
public boolean removeGroup(GroupModel group) {
+ if (realm.getDefaultGroups() != null) {
+ getMongoStore().pullItemFromList(realm, "defaultGroups", group.getId(), invocationContext);
+ }
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
@@ -689,6 +692,8 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
return getMongoStore().removeEntity(MongoGroupEntity.class, group.getId(), invocationContext);
}
+
+
@Override
public List getDefaultRoles() {
return realm.getDefaultRoles();
@@ -720,6 +725,27 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
updateRealm();
}
+ @Override
+ public List getDefaultGroups() {
+ List defaultGroups = new LinkedList<>();
+ for (String id : realm.getDefaultGroups()) {
+ defaultGroups.add(session.realms().getGroupById(id, this));
+ }
+ return defaultGroups;
+ }
+
+ @Override
+ public void addDefaultGroup(GroupModel group) {
+ getMongoStore().pushItemToList(realm, "defaultGroups", group.getId(), true, invocationContext);
+
+ }
+
+ @Override
+ public void removeDefaultGroup(GroupModel group) {
+ getMongoStore().pullItemFromList(realm, "defaultGroups", group.getId(), invocationContext);
+
+ }
+
@Override
public ClientModel getClientById(String id) {
return model.getClientById(id, this);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 9845031cc5..d874d1e2d5 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -634,6 +634,48 @@ public class RealmAdminResource {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
}
+ /**
+ * Get group hierarchy. Only name and ids are returned.
+ *
+ * @return
+ */
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("default-groups")
+ public List getDefaultGroups() {
+ this.auth.requireView();
+ List defaults = new LinkedList<>();
+ for (GroupModel group : realm.getDefaultGroups()) {
+ defaults.add(ModelToRepresentation.toRepresentation(group, false));
+ }
+ return defaults;
+ }
+ @PUT
+ @NoCache
+ @Path("default-groups/{groupId}")
+ public void addDefaultGroup(@PathParam("groupId") String groupId) {
+ this.auth.requireManage();
+ GroupModel group = realm.getGroupById(groupId);
+ if (group == null) {
+ throw new NotFoundException("Group not found");
+ }
+ realm.addDefaultGroup(group);
+ }
+
+ @DELETE
+ @NoCache
+ @Path("default-groups/{groupId}")
+ public void removeDefaultGroup(@PathParam("groupId") String groupId) {
+ this.auth.requireManage();
+ GroupModel group = realm.getGroupById(groupId);
+ if (group == null) {
+ throw new NotFoundException("Group not found");
+ }
+ realm.removeDefaultGroup(group);
+ }
+
+
@Path("groups")
public GroupsResource getGroups() {
GroupsResource resource = new GroupsResource(realm, session, this.auth, adminEvent);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
index d2882dc42f..97250c4f10 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
@@ -180,6 +180,25 @@ public class GroupTest {
Assert.assertTrue(token.getRealmAccess().getRoles().contains("level2Role"));
Assert.assertTrue(token.getRealmAccess().getRoles().contains("level3Role"));
+ realm.addDefaultGroup(level3Group.getId());
+
+ List defaultGroups = realm.getDefaultGroups();
+ Assert.assertEquals(1, defaultGroups.size());
+ Assert.assertEquals(defaultGroups.get(0).getId(), level3Group.getId());
+
+ UserRepresentation newUser = new UserRepresentation();
+ newUser.setUsername("groupUser");
+ newUser.setEmail("group@group.com");
+ response = realm.users().create(newUser);
+ response.close();
+ newUser = realm.users().search("groupUser", -1, -1).get(0);
+ membership = realm.users().get(newUser.getId()).groups();
+ Assert.assertEquals(1, membership.size());
+ Assert.assertEquals("level3", membership.get(0).getName());
+
+ realm.removeDefaultGroup(level3Group.getId());
+ defaultGroups = realm.getDefaultGroups();
+ Assert.assertEquals(0, defaultGroups.size());
}