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()); }