From d896800ec653d92699b430cf676e862fdf3116d7 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 29 Oct 2015 16:33:02 -0400 Subject: [PATCH] groups initial --- .../idm/GroupRepresentation.java | 81 +++++ .../idm/RealmRepresentation.java | 9 + .../BasePropertiesFederationProvider.java | 7 + ...ClasspathPropertiesFederationProvider.java | 2 + .../kerberos/KerberosFederationProvider.java | 6 + .../ldap/LDAPFederationProvider.java | 6 + .../RefreshableKeycloakSecurityContext.java | 3 +- .../java/org/keycloak/models/GroupModel.java | 74 ++++ .../java/org/keycloak/models/RealmModel.java | 8 + .../org/keycloak/models/RealmProvider.java | 5 +- .../models/UserFederationManager.java | 20 ++ .../models/UserFederationProvider.java | 8 + .../java/org/keycloak/models/UserModel.java | 5 + .../org/keycloak/models/UserProvider.java | 5 + .../keycloak/models/entities/GroupEntity.java | 60 ++++ .../keycloak/models/entities/UserEntity.java | 9 + .../models/utils/KeycloakModelUtils.java | 19 ++ .../models/utils/UserModelDelegate.java | 23 ++ .../models/file/FileRealmProvider.java | 6 + .../models/file/FileUserProvider.java | 16 + .../models/file/adapter/GroupAdapter.java | 208 ++++++++++++ .../models/file/adapter/RealmAdapter.java | 32 ++ .../models/file/adapter/UserAdapter.java | 25 ++ .../infinispan/DefaultCacheRealmProvider.java | 36 ++ .../infinispan/DefaultCacheUserProvider.java | 15 + .../models/cache/infinispan/GroupAdapter.java | 236 +++++++++++++ .../infinispan/InfinispanRealmCache.java | 38 +++ .../models/cache/infinispan/RealmAdapter.java | 38 +++ .../models/cache/infinispan/UserAdapter.java | 39 +++ .../models/cache/CacheRealmProvider.java | 2 + .../org/keycloak/models/cache/RealmCache.java | 11 + .../models/cache/entities/CachedGroup.java | 74 ++++ .../models/cache/entities/CachedRealm.java | 9 + .../models/cache/entities/CachedUser.java | 12 + .../org/keycloak/models/jpa/GroupAdapter.java | 320 ++++++++++++++++++ .../keycloak/models/jpa/JpaRealmProvider.java | 10 + .../keycloak/models/jpa/JpaUserProvider.java | 41 +++ .../org/keycloak/models/jpa/RealmAdapter.java | 58 ++++ .../org/keycloak/models/jpa/UserAdapter.java | 59 ++++ .../jpa/entities/GroupAttributeEntity.java | 70 ++++ .../models/jpa/entities/GroupEntity.java | 109 ++++++ .../jpa/entities/GroupRoleMappingEntity.java | 101 ++++++ .../models/jpa/entities/RealmEntity.java | 19 ++ .../entities/UserGroupMembershipEntity.java | 102 ++++++ .../mongo/keycloak/adapters/GroupAdapter.java | 225 ++++++++++++ .../keycloak/adapters/MongoRealmProvider.java | 10 + .../keycloak/adapters/MongoUserProvider.java | 28 ++ .../mongo/keycloak/adapters/RealmAdapter.java | 43 +++ .../mongo/keycloak/adapters/UserAdapter.java | 32 ++ .../keycloak/entities/MongoGroupEntity.java | 26 ++ .../models/mongo/utils/MongoModelUtils.java | 2 + .../DummyUserFederationProvider.java | 6 + 52 files changed, 2406 insertions(+), 2 deletions(-) create mode 100755 core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java mode change 100644 => 100755 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java create mode 100755 model/api/src/main/java/org/keycloak/models/GroupModel.java create mode 100755 model/api/src/main/java/org/keycloak/models/entities/GroupEntity.java create mode 100755 model/file/src/main/java/org/keycloak/models/file/adapter/GroupAdapter.java mode change 100644 => 100755 model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java mode change 100644 => 100755 model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java create mode 100755 model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java create mode 100755 model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedGroup.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java create mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java create mode 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java create mode 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoGroupEntity.java diff --git a/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java new file mode 100755 index 0000000000..833dfe3a5f --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java @@ -0,0 +1,81 @@ +package org.keycloak.representations.idm; + +import org.codehaus.jackson.annotate.JsonIgnore; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class GroupRepresentation { + private String id; + private String name; + protected Map attributes; + protected List realmRoles; + protected Map> clientRoles; + protected List subGroups; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getRealmRoles() { + return realmRoles; + } + + public void setRealmRoles(List realmRoles) { + this.realmRoles = realmRoles; + } + + public Map> getClientRoles() { + return clientRoles; + } + + public void setClientRoles(Map> clientRoles) { + this.clientRoles = clientRoles; + } + + public Map getAttributes() { + return attributes; + } + + // This method can be removed once we can remove backwards compatibility with Keycloak 1.3 (then getAttributes() can be changed to return Map> ) + @JsonIgnore + public Map> getAttributesAsListValues() { + return (Map) attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public GroupRepresentation singleAttribute(String name, String value) { + if (this.attributes == null) attributes = new HashMap<>(); + attributes.put(name, Arrays.asList(value)); + return this; + } + + public List getSubGroups() { + return subGroups; + } + + public void setSubGroups(List subGroups) { + this.subGroups = subGroups; + } +} 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 387ab7e783..e1333e9280 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -47,6 +47,7 @@ public class RealmRepresentation { protected String certificate; protected String codeSecret; protected RolesRepresentation roles; + protected List groups; protected List defaultRoles; @Deprecated protected Set requiredCredentials; @@ -775,4 +776,12 @@ public class RealmRepresentation { public void setClientAuthenticationFlow(String clientAuthenticationFlow) { this.clientAuthenticationFlow = clientAuthenticationFlow; } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } } diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java index f6228959c8..d6475aaa61 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java @@ -1,6 +1,7 @@ package org.keycloak.examples.federation.properties; import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -106,6 +107,12 @@ public abstract class BasePropertiesFederationProvider implements UserFederation } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + // complete we dont'care if a role is removed + + } + /** * See if the user is still in the properties file * diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java index 164cf79cdb..5467a425f0 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java @@ -63,4 +63,6 @@ public class ClasspathPropertiesFederationProvider extends BasePropertiesFederat throw new IllegalStateException("Remove not supported"); } + + } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java old mode 100644 new mode 100755 index ec1a905010..09a4da75b6 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -12,6 +12,7 @@ import org.jboss.logging.Logger; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -105,6 +106,11 @@ public class KerberosFederationProvider implements UserFederationProvider { } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + + } + @Override public boolean isValid(RealmModel realm, UserModel local) { // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 322155b61a..3704f7626f 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -12,6 +12,7 @@ import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelDuplicateException; @@ -319,6 +320,11 @@ public class LDAPFederationProvider implements UserFederationProvider { // TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper? } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + + } + public boolean validPassword(RealmModel realm, UserModel user, String password) { if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) { // Use Kerberos JAAS (Krb5LoginModule) diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java index b9bdb0dc2f..6e954e7cbb 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java @@ -57,7 +57,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext } public boolean isActive() { - return this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore(); + return token != null && this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore(); } public KeycloakDeployment getDeployment() { @@ -111,6 +111,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext log.debug("Token Verification succeeded!"); } catch (VerificationException e) { log.error("failed verification of token"); + return false; } if (response.getNotBeforePolicy() > deployment.getNotBefore()) { deployment.setNotBefore(response.getNotBeforePolicy()); diff --git a/model/api/src/main/java/org/keycloak/models/GroupModel.java b/model/api/src/main/java/org/keycloak/models/GroupModel.java new file mode 100755 index 0000000000..a1a63f8034 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/GroupModel.java @@ -0,0 +1,74 @@ +package org.keycloak.models; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface GroupModel { + String getId(); + + String getName(); + + void setName(String name); + + /** + * Set single value of specified attribute. Remove all other existing values + * + * @param name + * @param value + */ + void setSingleAttribute(String name, String value); + + void setAttribute(String name, List values); + + void removeAttribute(String name); + + /** + * @param name + * @return null if there is not any value of specified attribute or first value otherwise. Don't throw exception if there are more values of the attribute + */ + String getFirstAttribute(String name); + + /** + * @param name + * @return list of all attribute values or empty list if there are not any values. Never return null + */ + List getAttribute(String name); + + Map> getAttributes(); + + Set getRealmRoleMappings(); + Set getClientRoleMappings(ClientModel app); + boolean hasRole(RoleModel role); + void grantRole(RoleModel role); + Set getRoleMappings(); + void deleteRoleMapping(RoleModel role); + + GroupModel getParent(); + Set getSubGroups(); + + /** + * You must also call joinGroup on the parent group. + * + * @param group + */ + void setParent(GroupModel group); + + /** + * Automatically calls setParent() on the subGroup + * + * @param subGroup + */ + void addChild(GroupModel subGroup); + + /** + * Automatically calls setParent() on the subGroup + * + * @param subGroup + */ + void removeChild(GroupModel subGroup); +} 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 0737ee385b..43934ef8cb 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -328,4 +328,12 @@ public interface RealmModel extends RoleContainerModel { void setSupportedLocales(Set locales); String getDefaultLocale(); void setDefaultLocale(String locale); + + GroupModel getGroupById(String id); + List getGroups(); + List getTopLevelGroups(); + boolean removeGroup(GroupModel group); + + + } diff --git a/model/api/src/main/java/org/keycloak/models/RealmProvider.java b/model/api/src/main/java/org/keycloak/models/RealmProvider.java index 8e864cf874..f649f3509c 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmProvider.java +++ b/model/api/src/main/java/org/keycloak/models/RealmProvider.java @@ -20,8 +20,11 @@ public interface RealmProvider extends Provider { RoleModel getRoleById(String id, RealmModel realm); ClientModel getClientById(String id, RealmModel realm); + GroupModel getGroupById(String id, RealmModel realm); + + + List getRealms(); boolean removeRealm(String id); - void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index bea51e0473..b0ffba4752 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -165,6 +165,16 @@ public class UserFederationManager implements UserProvider { return user; } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return session.userStorage().getGroupMembers(realm, group, firstResult, maxResults); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return getGroupMembers(realm, group, -1, -1); + } + @Override public UserModel getUserByUsername(String username, RealmModel realm) { UserModel user = session.userStorage().getUserByUsername(username.toLowerCase(), realm); @@ -347,6 +357,16 @@ public class UserFederationManager implements UserProvider { session.userStorage().preRemove(realm, model); } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + for (UserFederationProviderModel federation : realm.getUserFederationProviders()) { + UserFederationProvider fed = getFederationProvider(federation); + fed.preRemove(realm, group); + } + session.userStorage().preRemove(realm, group); + + } + @Override public void preRemove(RealmModel realm, RoleModel role) { for (UserFederationProviderModel federation : realm.getUserFederationProviders()) { diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java index f60c21d7a7..043176e88b 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java @@ -119,6 +119,14 @@ public interface UserFederationProvider extends Provider { */ void preRemove(RealmModel realm, RoleModel role); + /** + * called before a role is removed. + * + * @param realm + * @param group + */ + void preRemove(RealmModel realm, GroupModel group); + /** * Is the Keycloak UserModel still valid and/or existing in federated storage? Keycloak may call this method * in various user operations. The local storage may be deleted if this method returns false. diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 9dec77844c..e35633d05e 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -101,6 +101,11 @@ public interface UserModel { Set getRoleMappings(); void deleteRoleMapping(RoleModel role); + Set getGroups(); + void joinGroup(GroupModel group); + void leaveGroup(GroupModel group); + boolean isMemberOf(GroupModel group); + String getFederationLink(); void setFederationLink(String link); diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index 7d7064d76e..2ad5c55f8a 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -25,12 +25,16 @@ public interface UserProvider extends Provider { UserModel getUserById(String id, RealmModel realm); UserModel getUserByUsername(String username, RealmModel realm); UserModel getUserByEmail(String email, RealmModel realm); + + List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults); + UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm); UserModel getUserByServiceAccountClient(ClientModel client); List getUsers(RealmModel realm, boolean includeServiceAccounts); // Service account is included for counts int getUsersCount(RealmModel realm); + List getGroupMembers(RealmModel realm, GroupModel group); List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); List searchForUser(String search, RealmModel realm); List searchForUser(String search, RealmModel realm, int firstResult, int maxResults); @@ -48,6 +52,7 @@ public interface UserProvider extends Provider { void preRemove(RealmModel realm, UserFederationProviderModel link); void preRemove(RealmModel realm, RoleModel role); + void preRemove(RealmModel realm, GroupModel group); void preRemove(RealmModel realm, ClientModel client); void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper); diff --git a/model/api/src/main/java/org/keycloak/models/entities/GroupEntity.java b/model/api/src/main/java/org/keycloak/models/entities/GroupEntity.java new file mode 100755 index 0000000000..5e64643e57 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/entities/GroupEntity.java @@ -0,0 +1,60 @@ +package org.keycloak.models.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke/a> + */ +public class GroupEntity extends AbstractIdentifiableEntity { + + private String name; + private String realmId; + + private List roleIds; + private String parentId; + private Map> attributes; + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public List getRoleIds() { + return roleIds; + } + + public void setRoleIds(List roleIds) { + this.roleIds = roleIds; + } + + public Map> getAttributes() { + return attributes; + } + + public void setAttributes(Map> attributes) { + this.attributes = attributes; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } +} + diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java index 8c82a8e13b..2c118beb06 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java @@ -21,6 +21,7 @@ public class UserEntity extends AbstractIdentifiableEntity { private String realmId; private List roleIds; + private List groupIds; private Map> attributes; private List requiredActions; @@ -157,5 +158,13 @@ public class UserEntity extends AbstractIdentifiableEntity { public void setServiceAccountClientLink(String serviceAccountClientLink) { this.serviceAccountClientLink = serviceAccountClientLink; } + + public List getGroupIds() { + return groupIds; + } + + public void setGroupIds(List groupIds) { + this.groupIds = groupIds; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 4b630d8019..8244467096 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -3,6 +3,7 @@ package org.keycloak.models.utils; import org.bouncycastle.openssl.PEMWriter; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; @@ -279,6 +280,24 @@ public final class KeycloakModelUtils { return false; } + /** + * + * @param groups + * @param targetGroup + * @return true if targetGroup is in groups (directly or indirectly via parent child relationship) + */ + public static boolean isMember(Set groups, GroupModel targetGroup) { + if (groups.contains(targetGroup)) return true; + + for (GroupModel mapping : groups) { + GroupModel child = mapping; + while(child.getParent() != null) { + if (child.getParent().equals(targetGroup)) return true; + child = child.getParent(); + } + } + return false; + } // USER FEDERATION RELATED STUFF /** diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 4cd162bce6..23ca7f673e 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -1,6 +1,7 @@ package org.keycloak.models.utils; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -255,4 +256,26 @@ public class UserModelDelegate implements UserModel { public void setCreatedTimestamp(Long timestamp){ delegate.setCreatedTimestamp(timestamp); } + + @Override + public Set getGroups() { + return delegate.getGroups(); + } + + @Override + public void joinGroup(GroupModel group) { + delegate.joinGroup(group); + + } + + @Override + public void leaveGroup(GroupModel group) { + delegate.leaveGroup(group); + + } + + @Override + public boolean isMemberOf(GroupModel group) { + return delegate.isMemberOf(group); + } } diff --git a/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java index 593aa88a1a..33d4fa36d3 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java @@ -20,6 +20,7 @@ import org.keycloak.connections.file.FileConnectionProvider; import org.keycloak.connections.file.InMemoryModel; import org.keycloak.migration.MigrationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; @@ -78,6 +79,11 @@ public class FileRealmProvider implements RealmProvider { return realm; } + @Override + public GroupModel getGroupById(String id, RealmModel realm) { + return null; + } + @Override public RealmModel getRealm(String id) { RealmModel model = inMemoryModel.getRealm(id); diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index 8edfe3ec2e..6540c37684 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -21,6 +21,7 @@ import org.keycloak.connections.file.InMemoryModel; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; @@ -80,6 +81,21 @@ public class FileUserProvider implements UserProvider { return inMemoryModel.getUser(realm.getId(), userId); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return null; + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return null; + } + + @Override + public void preRemove(RealmModel realm, GroupModel group) { + + } + @Override public UserModel getUserByUsername(String username, RealmModel realm) { for (UserModel user : inMemoryModel.getUsers(realm.getId())) { diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/GroupAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/GroupAdapter.java new file mode 100755 index 0000000000..ca10f6c901 --- /dev/null +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/GroupAdapter.java @@ -0,0 +1,208 @@ +package org.keycloak.models.file.adapter; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.entities.GroupEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author Marek Posolda + */ +public class GroupAdapter implements GroupModel { + + private final GroupEntity group; + private RealmModel realm; + private KeycloakSession session; + + public GroupAdapter(KeycloakSession session, RealmModel realm, GroupEntity group) { + this.group = group; + this.realm = realm; + this.session = session; + } + + @Override + public String getId() { + return group.getId(); + } + + @Override + public String getName() { + return group.getName(); + } + + @Override + public void setName(String name) { + group.setName(name); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof GroupModel)) return false; + + GroupModel that = (GroupModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public void setSingleAttribute(String name, String value) { + if (group.getAttributes() == null) { + group.setAttributes(new HashMap>()); + } + + List attrValues = new ArrayList<>(); + attrValues.add(value); + group.getAttributes().put(name, attrValues); + } + + @Override + public void setAttribute(String name, List values) { + if (group.getAttributes() == null) { + group.setAttributes(new HashMap>()); + } + + group.getAttributes().put(name, values); + } + + @Override + public void removeAttribute(String name) { + if (group.getAttributes() == null) return; + + group.getAttributes().remove(name); + } + + @Override + public String getFirstAttribute(String name) { + if (group.getAttributes()==null) return null; + + List attrValues = group.getAttributes().get(name); + return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0); + } + + @Override + public List getAttribute(String name) { + if (group.getAttributes()==null) return Collections.emptyList(); + List attrValues = group.getAttributes().get(name); + return (attrValues == null) ? Collections.emptyList() : Collections.unmodifiableList(attrValues); + } + + @Override + public Map> getAttributes() { + return group.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map) group.getAttributes()); + } + + @Override + public boolean hasRole(RoleModel role) { + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); + } + + @Override + public void grantRole(RoleModel role) { + if (group.getRoleIds() == null) { + group.setRoleIds(new LinkedList()); + } + if (group.getRoleIds().contains(role.getId())) { + return; + } + group.getRoleIds().add(role.getId()); + } + + @Override + public Set getRoleMappings() { + if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET; + Set roles = new HashSet<>(); + for (String id : group.getRoleIds()) { + roles.add(realm.getRoleById(id)); + } + return roles; + } + + @Override + public Set getRealmRoleMappings() { + Set allRoles = getRoleMappings(); + + // Filter to retrieve just realm roles + Set realmRoles = new HashSet(); + for (RoleModel role : allRoles) { + if (role.getContainer() instanceof RealmModel) { + realmRoles.add(role); + } + } + return realmRoles; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + if (group == null || role == null) return; + if (group.getRoleIds() == null) return; + group.getRoleIds().remove(role.getId()); + } + + @Override + public Set getClientRoleMappings(ClientModel app) { + Set result = new HashSet(); + Set roles = getRoleMappings(); + + for (RoleModel role : roles) { + if (app.equals(role.getContainer())) { + result.add(role); + } + } + return result; + } + + @Override + public GroupModel getParent() { + if (group.getParentId() == null) return null; + return realm.getGroupById(group.getParentId()); + } + + @Override + public Set getSubGroups() { + Set subGroups = new HashSet<>(); + for (GroupModel groupModel : realm.getGroups()) { + if (groupModel.getParent().equals(this)) { + subGroups.add(groupModel); + } + } + return subGroups; + } + + @Override + public void setParent(GroupModel group) { + this.group.setParentId(group.getId()); + + } + + @Override + public void addChild(GroupModel subGroup) { + subGroup.setParent(this); + + } + + @Override + public void removeChild(GroupModel subGroup) { + subGroup.setParent(null); + + } +} diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index 584348074e..4f24afd867 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -22,6 +22,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -88,6 +89,7 @@ public class RealmAdapter implements RealmModel { private final Map allApps = new HashMap(); private ClientModel masterAdminApp = null; private final Map allRoles = new HashMap(); + private final Map allGroups = new HashMap(); private final Map allIdProviders = new HashMap(); public RealmAdapter(KeycloakSession session, RealmEntity realm, InMemoryModel inMemoryModel) { @@ -601,6 +603,36 @@ public class RealmAdapter implements RealmModel { return null; } + @Override + public GroupModel getGroupById(String id) { + GroupModel found = allGroups.get(id); + if (found != null) return found; + return null; + } + + @Override + public List getGroups() { + List list = new LinkedList<>(); + for (GroupAdapter group : allGroups.values()) { + list.add(group); + } + return list; + } + + @Override + public List getTopLevelGroups() { + List list = new LinkedList<>(); + for (GroupAdapter group : allGroups.values()) { + if (group.getParent() == null) list.add(group); + } + return list; + } + + @Override + public boolean removeGroup(GroupModel group) { + return allGroups.remove(group.getId()) != null; + } + @Override public List getDefaultRoles() { return realm.getDefaultRoles(); diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index 9a3379c610..d0dc92b756 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -21,6 +21,7 @@ import org.keycloak.models.ClientModel; import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; +import org.keycloak.models.GroupModel; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.OTPPolicy; import org.keycloak.models.UserConsentModel; @@ -59,6 +60,7 @@ public class UserAdapter implements UserModel, Comparable { private final RealmModel realm; private final Set allRoles = new HashSet(); + private final Set allGroups = new HashSet(); public UserAdapter(RealmModel realm, UserEntity userEntity, InMemoryModel inMemoryModel) { this.user = userEntity; @@ -467,6 +469,29 @@ public class UserAdapter implements UserModel, Comparable { credentialEntity.setPeriod(credModel.getPeriod()); } + @Override + public Set getGroups() { + return Collections.unmodifiableSet(allGroups); + } + + @Override + public void joinGroup(GroupModel group) { + allGroups.add(group); + + } + + @Override + public void leaveGroup(GroupModel group) { + if (user == null || group == null) return; + allGroups.remove(group); + + } + + @Override + public boolean isMemberOf(GroupModel group) { + return KeycloakModelUtils.isMember(getGroups(), group); + } + @Override public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java old mode 100644 new mode 100755 index b84079f25f..3e45a69f75 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java @@ -21,9 +21,11 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { protected Set realmInvalidations = new HashSet(); protected Set appInvalidations = new HashSet(); protected Set roleInvalidations = new HashSet(); + protected Set groupInvalidations = new HashSet(); protected Map managedRealms = new HashMap(); protected Map managedApplications = new HashMap(); protected Map managedRoles = new HashMap(); + protected Map managedGroups = new HashMap(); protected boolean clearAll; @@ -73,6 +75,12 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { roleInvalidations.add(id); } + @Override + public void registerGroupInvalidation(String id) { + groupInvalidations.add(id); + + } + protected void runInvalidations() { for (String id : realmInvalidations) { cache.invalidateCachedRealmById(id); @@ -80,6 +88,9 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { for (String id : roleInvalidations) { cache.invalidateRoleById(id); } + for (String id : groupInvalidations) { + cache.invalidateGroupById(id); + } for (String id : appInvalidations) { cache.invalidateCachedApplicationById(id); } @@ -254,6 +265,31 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { return adapter; } + @Override + public GroupModel getGroupById(String id, RealmModel realm) { + if (!cache.isEnabled()) return getDelegate().getGroupById(id, realm); + CachedGroup cached = cache.getGroup(id); + if (cached != null && !cached.getRealm().equals(realm.getId())) { + cached = null; + } + + if (cached == null) { + GroupModel model = getDelegate().getGroupById(id, realm); + if (model == null) return null; + if (groupInvalidations.contains(id)) return model; + cached = new CachedGroup(realm, model); + cache.addCachedGroup(cached); + + } else if (groupInvalidations.contains(id)) { + return getDelegate().getGroupById(id, realm); + } else if (managedGroups.containsKey(id)) { + return managedGroups.get(id); + } + GroupAdapter adapter = new GroupAdapter(cached, this, session, realm); + managedGroups.put(id, adapter); + return adapter; + } + @Override public ClientModel getClientById(String id, RealmModel realm) { if (!cache.isEnabled()) return getDelegate().getClientById(id, realm); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java old mode 100644 new mode 100755 index 69fc5cf577..3ea488d4b9 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java @@ -197,6 +197,16 @@ public class DefaultCacheUserProvider implements CacheUserProvider { return getDelegate().getUserByFederatedIdentity(socialLink, realm); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return getDelegate().getGroupMembers(realm, group, firstResult, maxResults); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return getDelegate().getGroupMembers(realm, group); + } + @Override public UserModel getUserByServiceAccountClient(ClientModel client) { return getDelegate().getUserByServiceAccountClient(client); @@ -313,6 +323,11 @@ public class DefaultCacheUserProvider implements CacheUserProvider { public void preRemove(RealmModel realm, RoleModel role) { getDelegate().preRemove(realm, role); } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + getDelegate().preRemove(realm, group); + } + @Override public void preRemove(RealmModel realm, UserFederationProviderModel link) { diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java new file mode 100755 index 0000000000..4712272be6 --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java @@ -0,0 +1,236 @@ +package org.keycloak.models.cache.infinispan; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CacheRealmProvider; +import org.keycloak.models.cache.CacheUserProvider; +import org.keycloak.models.cache.entities.CachedGroup; +import org.keycloak.models.cache.entities.CachedUser; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class GroupAdapter implements GroupModel { + protected GroupModel updated; + protected CachedGroup cached; + protected CacheRealmProvider cacheSession; + protected KeycloakSession keycloakSession; + protected RealmModel realm; + + public GroupAdapter(CachedGroup cached, CacheRealmProvider cacheSession, KeycloakSession keycloakSession, RealmModel realm) { + this.cached = cached; + this.cacheSession = cacheSession; + this.keycloakSession = keycloakSession; + this.realm = realm; + } + + protected void getDelegateForUpdate() { + if (updated == null) { + cacheSession.registerGroupInvalidation(getId()); + updated = cacheSession.getDelegate().getGroupById(getId(), realm); + if (updated == null) throw new IllegalStateException("Not found in database"); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof GroupModel)) return false; + + GroupModel that = (GroupModel) o; + + if (!cached.getId().equals(that.getId())) return false; + + return true; + } + + @Override + public int hashCode() { + return cached.getId().hashCode(); + } + + @Override + public String getId() { + if (updated != null) return updated.getId(); + return cached.getId(); + } + + @Override + public String getName() { + if (updated != null) return updated.getName(); + return cached.getName(); + } + + @Override + public void setName(String name) { + getDelegateForUpdate(); + updated.setName(name); + + } + + @Override + public void setSingleAttribute(String name, String value) { + getDelegateForUpdate(); + updated.setSingleAttribute(name, value); + } + + @Override + public void setAttribute(String name, List values) { + getDelegateForUpdate(); + updated.setAttribute(name, values); + } + + @Override + public void removeAttribute(String name) { + getDelegateForUpdate(); + updated.removeAttribute(name); + + } + + @Override + public String getFirstAttribute(String name) { + if (updated != null) return updated.getFirstAttribute(name); + return cached.getAttributes().getFirst(name); + } + + @Override + public List getAttribute(String name) { + List values = cached.getAttributes().get(name); + if (values == null) return null; + return values; + } + + @Override + public Map> getAttributes() { + return cached.getAttributes(); + } + + @Override + public Set getRealmRoleMappings() { + if (updated != null) return updated.getRealmRoleMappings(); + Set roleMappings = getRoleMappings(); + Set realmMappings = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof RealmModel) { + if (((RealmModel) container).getId().equals(realm.getId())) { + realmMappings.add(role); + } + } + } + return realmMappings; + } + + @Override + public Set getClientRoleMappings(ClientModel app) { + if (updated != null) return updated.getClientRoleMappings(app); + Set roleMappings = getRoleMappings(); + Set appMappings = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof ClientModel) { + if (((ClientModel) container).getId().equals(app.getId())) { + appMappings.add(role); + } + } + } + return appMappings; + } + + @Override + public boolean hasRole(RoleModel role) { + if (updated != null) return updated.hasRole(role); + if (cached.getRoleMappings().contains(role.getId())) return true; + + Set mappings = getRoleMappings(); + for (RoleModel mapping: mappings) { + if (mapping.hasRole(role)) return true; + } + return false; + } + + @Override + public void grantRole(RoleModel role) { + getDelegateForUpdate(); + updated.grantRole(role); + } + + @Override + public Set getRoleMappings() { + if (updated != null) return updated.getRoleMappings(); + Set roles = new HashSet(); + for (String id : cached.getRoleMappings()) { + RoleModel roleById = keycloakSession.realms().getRoleById(id, realm); + if (roleById == null) { + // chance that role was removed, so just delegate to persistence and get user invalidated + getDelegateForUpdate(); + return updated.getRoleMappings(); + } + roles.add(roleById); + + } + return roles; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + getDelegateForUpdate(); + updated.deleteRoleMapping(role); + } + + @Override + public GroupModel getParent() { + if (updated != null) return updated.getParent(); + if (cached.getParentId() == null) return null; + return keycloakSession.realms().getGroupById(cached.getParentId(), realm); + } + + @Override + public Set getSubGroups() { + if (updated != null) return updated.getSubGroups(); + Set subGroups = new HashSet<>(); + for (String id : cached.getSubGroups()) { + GroupModel subGroup = keycloakSession.realms().getGroupById(id, realm); + if (subGroup == null) { + // chance that role was removed, so just delegate to persistence and get user invalidated + getDelegateForUpdate(); + return updated.getSubGroups(); + + } + subGroups.add(subGroup); + } + return subGroups; + } + + @Override + public void setParent(GroupModel group) { + getDelegateForUpdate(); + updated.setParent(group); + + } + + @Override + public void addChild(GroupModel subGroup) { + getDelegateForUpdate(); + updated.addChild(subGroup); + + } + + @Override + public void removeChild(GroupModel subGroup) { + getDelegateForUpdate(); + updated.removeChild(subGroup); + } +} diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java index 07554a889e..a5239fbcf5 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java @@ -4,6 +4,7 @@ import org.infinispan.Cache; import org.jboss.logging.Logger; import org.keycloak.models.cache.RealmCache; import org.keycloak.models.cache.entities.CachedClient; +import org.keycloak.models.cache.entities.CachedGroup; import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRole; @@ -101,12 +102,49 @@ public class InfinispanRealmCache implements RealmCache { cache.remove(id); } + @Override + public CachedGroup getGroup(String id) { + if (!enabled) return null; + return get(id, CachedGroup.class); + } + + @Override + public void invalidateGroup(CachedGroup role) { + logger.tracev("Removing group {0}", role.getId()); + cache.remove(role.getId()); + + } + + @Override + public void addCachedGroup(CachedGroup role) { + if (!enabled) return; + logger.tracev("Adding group {0}", role.getId()); + cache.put(role.getId(), role); + + } + + @Override + public void invalidateCachedGroupById(String id) { + logger.tracev("Removing group {0}", id); + cache.remove(id); + + } + + @Override + public void invalidateGroupById(String id) { + logger.tracev("Removing group {0}", id); + cache.remove(id); + + } + @Override public CachedRole getRole(String id) { if (!enabled) return null; return get(id, CachedRole.class); } + + @Override public void invalidateRole(CachedRole role) { logger.tracev("Removing role {0}", role.getId()); 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 13ec945ee7..14c111d445 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 @@ -1262,4 +1262,42 @@ public class RealmAdapter implements RealmModel { if (updated != null) return updated.getRequiredActionProviderByAlias(alias); return cached.getRequiredActionProvidersByAlias().get(alias); } + + @Override + public GroupModel getGroupById(String id) { + if (updated != null) return updated.getGroupById(id); + return cacheSession.getGroupById(id, this); + } + + @Override + public List getGroups() { + if (updated != null) return updated.getGroups(); + if (cached.getGroups().isEmpty()) return null; + List list = new LinkedList<>(); + for (String id : cached.getGroups()) { + GroupModel group = cacheSession.getGroupById(id, this); + if (group == null) continue; + list.add(group); + } + return list; + } + + @Override + public List getTopLevelGroups() { + List all = getGroups(); + Iterator it = all.iterator(); + while (it.hasNext()) { + GroupModel group = it.next(); + if (group.getParent() != null) { + it.remove(); + } + } + return all; + } + + @Override + public boolean removeGroup(GroupModel group) { + getDelegateForUpdate(); + return updated.removeGroup(group); + } } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 5a74b01a35..7113fde9a5 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -317,6 +317,44 @@ public class UserAdapter implements UserModel { updated.deleteRoleMapping(role); } + @Override + public Set getGroups() { + if (updated != null) return updated.getGroups(); + Set groups = new HashSet(); + for (String id : cached.getRoleMappings()) { + GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm); + if (groupModel == null) { + // chance that role was removed, so just delete to persistence and get user invalidated + getDelegateForUpdate(); + return updated.getGroups(); + } + groups.add(groupModel); + + } + return groups; + } + + @Override + public void joinGroup(GroupModel group) { + getDelegateForUpdate(); + updated.joinGroup(group); + + } + + @Override + public void leaveGroup(GroupModel group) { + getDelegateForUpdate(); + updated.leaveGroup(group); + } + + @Override + public boolean isMemberOf(GroupModel group) { + if (updated != null) return updated.isMemberOf(group); + if (cached.getGroups().contains(group.getId())) return true; + Set roles = getGroups(); + return KeycloakModelUtils.isMember(roles, group); + } + @Override public void addConsent(UserConsentModel consent) { getDelegateForUpdate(); @@ -348,4 +386,5 @@ public class UserAdapter implements UserModel { getDelegateForUpdate(); return updated.revokeConsentForClient(clientId); } + } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java index 4aab371699..26ad12de2d 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java @@ -17,4 +17,6 @@ public interface CacheRealmProvider extends RealmProvider { void registerApplicationInvalidation(String id); void registerRoleInvalidation(String id); + + void registerGroupInvalidation(String id); } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java index 9c895c376f..df74b536af 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java @@ -1,6 +1,7 @@ package org.keycloak.models.cache; import org.keycloak.models.cache.entities.CachedClient; +import org.keycloak.models.cache.entities.CachedGroup; import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRole; @@ -39,6 +40,16 @@ public interface RealmCache { void invalidateRoleById(String id); + CachedGroup getGroup(String id); + + void invalidateGroup(CachedGroup role); + + void addCachedGroup(CachedGroup role); + + void invalidateCachedGroupById(String id); + + void invalidateGroupById(String id); + boolean isEnabled(); void setEnabled(boolean enabled); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedGroup.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedGroup.java new file mode 100755 index 0000000000..7f16e948de --- /dev/null +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedGroup.java @@ -0,0 +1,74 @@ +package org.keycloak.models.cache.entities; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.GroupModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class CachedGroup implements Serializable { + private String id; + private String realm; + private String name; + private String parentId; + private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); + private Set roleMappings = new HashSet<>(); + private Set subGroups = new HashSet<>(); + + public CachedGroup(RealmModel realm, GroupModel group) { + this.id = group.getId(); + this.realm = realm.getId(); + this.name = group.getName(); + if (group.getParent() != null) this.parentId = group.getParent().getId(); + + this.attributes.putAll(group.getAttributes()); + for (RoleModel role : group.getRoleMappings()) { + roleMappings.add(role.getId()); + } + Set subGroups1 = group.getSubGroups(); + if (subGroups1 != null) { + for (GroupModel subGroup : subGroups1) { + subGroups.add(subGroup.getId()); + } + } + } + + public String getId() { + return id; + } + + public String getRealm() { + return realm; + } + + public MultivaluedHashMap getAttributes() { + return attributes; + } + + public Set getRoleMappings() { + return roleMappings; + } + + public String getName() { + return name; + } + + public String getParentId() { + return parentId; + } + + public Set getSubGroups() { + return subGroups; + } +} 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 18b8540e4d..604d7fa825 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 @@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.OTPPolicy; @@ -106,6 +107,7 @@ public class CachedRealm implements Serializable { protected Set adminEnabledEventOperations = new HashSet(); protected boolean adminEventsDetailsEnabled; private List defaultRoles = new LinkedList(); + private Set groups = new HashSet(); private Map realmRoles = new HashMap(); private Map clients = new HashMap(); private boolean internationalizationEnabled; @@ -216,6 +218,9 @@ public class CachedRealm implements Serializable { executionsById.put(execution.getId(), execution); } } + for (GroupModel group : model.getGroups()) { + groups.add(group.getId()); + } for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) { authenticatorConfigs.put(authenticator.getId(), authenticator); } @@ -507,4 +512,8 @@ public class CachedRealm implements Serializable { public AuthenticationFlowModel getClientAuthenticationFlow() { return clientAuthenticationFlow; } + + public Set getGroups() { + return groups; + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 0b5fc78496..fcd36c1b27 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -1,5 +1,6 @@ package org.keycloak.models.cache.entities; +import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialValueModel; @@ -33,6 +34,7 @@ public class CachedUser implements Serializable { private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); private Set requiredActions = new HashSet<>(); private Set roleMappings = new HashSet<>(); + private Set groups = new HashSet<>(); public CachedUser(RealmModel realm, UserModel user) { this.id = user.getId(); @@ -53,6 +55,12 @@ public class CachedUser implements Serializable { for (RoleModel role : user.getRoleMappings()) { roleMappings.add(role.getId()); } + Set groupMappings = user.getGroups(); + if (groupMappings != null) { + for (GroupModel group : groupMappings) { + groups.add(group.getId()); + } + } } public String getId() { @@ -118,4 +126,8 @@ public class CachedUser implements Serializable { public String getServiceAccountClientLink() { return serviceAccountClientLink; } + + public Set getGroups() { + return groups; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java new file mode 100755 index 0000000000..0e2e64407a --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java @@ -0,0 +1,320 @@ +package org.keycloak.models.jpa; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ModelException; +import org.keycloak.models.OTPPolicy; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserConsentModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.jpa.entities.CredentialEntity; +import org.keycloak.models.jpa.entities.GroupAttributeEntity; +import org.keycloak.models.jpa.entities.GroupEntity; +import org.keycloak.models.jpa.entities.GroupRoleMappingEntity; +import org.keycloak.models.jpa.entities.RoleEntity; +import org.keycloak.models.jpa.entities.UserAttributeEntity; +import org.keycloak.models.jpa.entities.UserConsentEntity; +import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity; +import org.keycloak.models.jpa.entities.UserConsentRoleEntity; +import org.keycloak.models.jpa.entities.UserEntity; +import org.keycloak.models.jpa.entities.UserRequiredActionEntity; +import org.keycloak.models.jpa.entities.UserRoleMappingEntity; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.Pbkdf2PasswordEncoder; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class GroupAdapter implements GroupModel { + + protected GroupEntity group; + protected EntityManager em; + protected RealmModel realm; + + public GroupAdapter(RealmModel realm, EntityManager em, GroupEntity group) { + this.em = em; + this.group = group; + this.realm = realm; + } + + public GroupEntity getGroup() { + return group; + } + + @Override + public String getId() { + return group.getId(); + } + + @Override + public String getName() { + return group.getName(); + } + + @Override + public void setName(String name) { + group.setName(name); + } + + @Override + public GroupModel getParent() { + GroupEntity parent = group.getParent(); + if (parent == null) return null; + return realm.getGroupById(parent.getId()); + } + + public static GroupEntity toEntity(GroupModel model, EntityManager em) { + if (model instanceof GroupAdapter) { + return ((GroupAdapter)model).getGroup(); + } + return em.getReference(GroupEntity.class, model.getId()); + } + + @Override + public void setParent(GroupModel group) { + GroupEntity parent = toEntity(group, em); + group.setParent(group); + } + + @Override + public void addChild(GroupModel subGroup) { + subGroup.setParent(this); + + } + + @Override + public void removeChild(GroupModel subGroup) { + subGroup.setParent(null); + + } + + @Override + public Set getSubGroups() { + TypedQuery query = em.createNamedQuery("getGroupIdsByParent", String.class); + query.setParameter("parent", group); + List ids = query.getResultList(); + Set set = new HashSet<>(); + for (String id : ids) { + GroupModel subGroup = realm.getGroupById(id); + if (subGroup == null) continue; + set.add(subGroup); + } + return set; + } + + @Override + public void setSingleAttribute(String name, String value) { + boolean found = false; + List toRemove = new ArrayList<>(); + for (GroupAttributeEntity attr : group.getAttributes()) { + if (attr.getName().equals(name)) { + if (!found) { + attr.setValue(value); + found = true; + } else { + toRemove.add(attr); + } + } + } + + for (GroupAttributeEntity attr : toRemove) { + em.remove(attr); + group.getAttributes().remove(attr); + } + + if (found) { + return; + } + + persistAttributeValue(name, value); + } + + @Override + public void setAttribute(String name, List values) { + // Remove all existing + removeAttribute(name); + + // Put all new + for (String value : values) { + persistAttributeValue(name, value); + } + } + + private void persistAttributeValue(String name, String value) { + GroupAttributeEntity attr = new GroupAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setName(name); + attr.setValue(value); + attr.setGroup(group); + em.persist(attr); + group.getAttributes().add(attr); + } + + @Override + public void removeAttribute(String name) { + Iterator it = group.getAttributes().iterator(); + while (it.hasNext()) { + GroupAttributeEntity attr = it.next(); + if (attr.getName().equals(name)) { + it.remove(); + em.remove(attr); + } + } + } + + @Override + public String getFirstAttribute(String name) { + for (GroupAttributeEntity attr : group.getAttributes()) { + if (attr.getName().equals(name)) { + return attr.getValue(); + } + } + return null; + } + + @Override + public List getAttribute(String name) { + List result = new ArrayList<>(); + for (GroupAttributeEntity attr : group.getAttributes()) { + if (attr.getName().equals(name)) { + result.add(attr.getValue()); + } + } + return result; + } + + @Override + public Map> getAttributes() { + MultivaluedHashMap result = new MultivaluedHashMap<>(); + for (GroupAttributeEntity attr : group.getAttributes()) { + result.add(attr.getName(), attr.getValue()); + } + return result; + } + + @Override + public boolean hasRole(RoleModel role) { + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); + } + + protected TypedQuery getGroupRoleMappingEntityTypedQuery(RoleModel role) { + TypedQuery query = em.createNamedQuery("groupHasRole", GroupRoleMappingEntity.class); + query.setParameter("group", getGroup()); + query.setParameter("roleId", role.getId()); + return query; + } + + @Override + public void grantRole(RoleModel role) { + if (hasRole(role)) return; + GroupRoleMappingEntity entity = new GroupRoleMappingEntity(); + entity.setGroup(getGroup()); + entity.setRoleId(role.getId()); + em.persist(entity); + em.flush(); + em.detach(entity); + } + + @Override + public Set getRealmRoleMappings() { + Set roleMappings = getRoleMappings(); + + Set realmRoles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof RealmModel) { + realmRoles.add(role); + } + } + return realmRoles; + } + + + @Override + public Set getRoleMappings() { + // we query ids only as the role might be cached and following the @ManyToOne will result in a load + // even if we're getting just the id. + TypedQuery query = em.createNamedQuery("groupRoleMappingIds", String.class); + query.setParameter("group", getGroup()); + List ids = query.getResultList(); + Set roles = new HashSet(); + for (String roleId : ids) { + RoleModel roleById = realm.getRoleById(roleId); + if (roleById == null) continue; + roles.add(roleById); + } + return roles; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + if (group == null || role == null) return; + + TypedQuery query = getGroupRoleMappingEntityTypedQuery(role); + List results = query.getResultList(); + if (results.size() == 0) return; + for (GroupRoleMappingEntity entity : results) { + em.remove(entity); + } + em.flush(); + } + + @Override + public Set getClientRoleMappings(ClientModel app) { + Set roleMappings = getRoleMappings(); + + Set roles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof ClientModel) { + ClientModel appModel = (ClientModel)container; + if (appModel.getId().equals(app.getId())) { + roles.add(role); + } + } + } + return roles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof UserModel)) return false; + + UserModel that = (UserModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index b9229bbb11..c5396cb50b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -2,11 +2,13 @@ package org.keycloak.models.jpa; import org.keycloak.migration.MigrationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.GroupEntity; import org.keycloak.models.jpa.entities.RealmEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.utils.KeycloakModelUtils; @@ -117,6 +119,14 @@ public class JpaRealmProvider implements RealmProvider { return new RoleAdapter(realm, em, entity); } + @Override + public GroupModel getGroupById(String id, RealmModel realm) { + GroupEntity groupEntity = em.find(GroupEntity.class, id); + if (groupEntity == null) return null; + if (groupEntity.getRealm().getId().equals(realm.getId())) return null; + return new GroupAdapter(realm, em, groupEntity); + } + @Override public ClientModel getClientById(String id, RealmModel realm) { ClientEntity app = em.find(ClientEntity.class, id); 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 d4d533a9dc..b643bb6d0c 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 @@ -3,6 +3,7 @@ package org.keycloak.models.jpa; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -169,6 +170,8 @@ public class JpaUserProvider implements UserProvider { .setParameter("realmId", realm.getId()).executeUpdate(); num = em.createNamedQuery("deleteUsersByRealm") .setParameter("realmId", realm.getId()).executeUpdate(); + num = em.createNamedQuery("deleteUserGroupMembershipByRealm") + .setParameter("realmId", realm.getId()).executeUpdate(); } @Override @@ -219,6 +222,25 @@ public class JpaUserProvider implements UserProvider { .executeUpdate(); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + TypedQuery query = em.createNamedQuery("groupMembership", UserEntity.class); + query.setParameter("groupId", group.getId()); + List results = query.getResultList(); + + List users = new ArrayList(); + for (UserEntity user : results) { + users.add(new UserAdapter(realm, em, user)); + } + return users; + } + + @Override + public void preRemove(RealmModel realm, GroupModel group) { + em.createNamedQuery("deleteUserGroupMembershipsByGroup").setParameter("groupId", group.getId()).executeUpdate(); + + } + @Override public UserModel getUserById(String id, RealmModel realm) { TypedQuery query = em.createNamedQuery("getRealmUserById", UserEntity.class); @@ -318,6 +340,25 @@ public class JpaUserProvider implements UserProvider { return users; } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + TypedQuery query = em.createNamedQuery("groupMembership", UserEntity.class); + query.setParameter("groupId", group.getId()); + if (firstResult != -1) { + query.setFirstResult(firstResult); + } + if (maxResults != -1) { + query.setMaxResults(maxResults); + } + List results = query.getResultList(); + + List users = new ArrayList(); + for (UserEntity user : results) { + users.add(new UserAdapter(realm, em, user)); + } + return users; + } + @Override public List searchForUser(String search, RealmModel realm) { return searchForUser(search, realm, -1, -1); 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 86f4490a03..385ad7e872 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 @@ -6,6 +6,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -24,6 +25,7 @@ import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity; import org.keycloak.models.jpa.entities.AuthenticationFlowEntity; import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity; import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.GroupEntity; import org.keycloak.models.jpa.entities.IdentityProviderEntity; import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity; import org.keycloak.models.jpa.entities.RealmAttributeEntity; @@ -1944,4 +1946,60 @@ public class RealmAdapter implements RealmModel { } return null; } + + @Override + public GroupModel getGroupById(String id) { + GroupEntity groupEntity = em.find(GroupEntity.class, id); + if (groupEntity == null) return null; + if (groupEntity.getRealm().getId().equals(getId())) return null; + return new GroupAdapter(this, em, groupEntity); + } + + @Override + public List getGroups() { + List list = new LinkedList<>(); + Collection groups = realm.getGroups(); + if (groups == null) return list; + for (GroupEntity entity : groups) { + list.add(new GroupAdapter(this, em, entity)); + } + return list; + } + + @Override + public List getTopLevelGroups() { + List all = getGroups(); + Iterator it = all.iterator(); + while (it.hasNext()) { + GroupModel group = it.next(); + if (group.getParent() != null) { + it.remove(); + } + } + return all; + } + + @Override + public boolean removeGroup(GroupModel group) { + if (group == null) { + return false; + } + GroupEntity groupEntity = GroupAdapter.toEntity(group, em); + if (!groupEntity.getRealm().getId().equals(getId())) { + return false; + } + for (GroupModel subGroup : group.getSubGroups()) { + removeGroup(subGroup); + } + + + session.users().preRemove(this, group); + realm.getGroups().remove(groupEntity); + em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", group).executeUpdate(); + em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", group).executeUpdate(); + em.remove(groupEntity); + return true; + + + } } \ No newline at end of file diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index b370886ccb..5246890979 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -1,6 +1,7 @@ package org.keycloak.models.jpa; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.OTPPolicy; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserConsentModel; @@ -19,6 +20,7 @@ import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity; import org.keycloak.models.jpa.entities.UserConsentRoleEntity; import org.keycloak.models.jpa.entities.UserAttributeEntity; import org.keycloak.models.jpa.entities.UserEntity; +import org.keycloak.models.jpa.entities.UserGroupMembershipEntity; import org.keycloak.models.jpa.entities.UserRequiredActionEntity; import org.keycloak.models.jpa.entities.UserRoleMappingEntity; import org.keycloak.models.utils.KeycloakModelUtils; @@ -485,6 +487,63 @@ public class UserAdapter implements UserModel { em.flush(); } + + @Override + public Set getGroups() { + // we query ids only as the group might be cached and following the @ManyToOne will result in a load + // even if we're getting just the id. + TypedQuery query = em.createNamedQuery("userGroupIds", String.class); + query.setParameter("user", getUser()); + List ids = query.getResultList(); + Set groups = new HashSet<>(); + for (String groupId : ids) { + GroupModel group = realm.getGroupById(groupId); + if (group == null) continue; + groups.add(group); + } + return groups; + } + + @Override + public void joinGroup(GroupModel group) { + if (isMemberOf(group)) return; + UserGroupMembershipEntity entity = new UserGroupMembershipEntity(); + entity.setUser(getUser()); + entity.setGroupId(group.getId()); + em.persist(entity); + em.flush(); + em.detach(entity); + + } + + @Override + public void leaveGroup(GroupModel group) { + if (user == null || group == null) return; + + TypedQuery query = getUserGroupMappingQuery(group); + List results = query.getResultList(); + if (results.size() == 0) return; + for (UserGroupMembershipEntity entity : results) { + em.remove(entity); + } + em.flush(); + + } + + @Override + public boolean isMemberOf(GroupModel group) { + Set roles = getGroups(); + return KeycloakModelUtils.isMember(roles, group); + } + + protected TypedQuery getUserGroupMappingQuery(GroupModel group) { + TypedQuery query = em.createNamedQuery("userMemberOf", UserGroupMembershipEntity.class); + query.setParameter("user", getUser()); + query.setParameter("groupId", group.getId()); + return query; + } + + @Override public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java new file mode 100755 index 0000000000..8ee79e093d --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java @@ -0,0 +1,70 @@ +package org.keycloak.models.jpa.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@NamedQueries({ + @NamedQuery(name="getGroupAttributesByNameAndValue", query="select attr from GroupAttributeEntity attr where attr.name = :name and attr.value = :value"), + @NamedQuery(name="deleteGroupAttributesByGroup", query="delete from GroupAttributeEntity attr where attr.group = :group"), + @NamedQuery(name="deleteGroupAttributesByRealm", query="delete from GroupAttributeEntity attr where attr.group IN (select u from GroupEntity u where u.realmId=:realmId)") +}) +@Table(name="USER_ATTRIBUTE") +@Entity +public class GroupAttributeEntity { + + @Id + @Column(name="ID", length = 36) + protected String id; + + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name = "GROUP_ID") + protected GroupEntity group; + + @Column(name = "NAME") + protected String name; + @Column(name = "VALUE") + protected String value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public GroupEntity getGroup() { + return group; + } + + public void setGroup(GroupEntity group) { + this.group = group; + } +} 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 new file mode 100755 index 0000000000..b944379cea --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java @@ -0,0 +1,109 @@ +package org.keycloak.models.jpa.entities; + +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.util.ArrayList; +import java.util.Collection; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@NamedQueries({ + @NamedQuery(name="getAllGroupsByRealm", query="select u from GroupEntity u where u.realmId = :realmId order by u.name"), + @NamedQuery(name="getGroupById", query="select u from GroupEntity u where u.id = :id and u.realmId = :realmId"), + @NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"), + @NamedQuery(name="getGroupByName", query="select u from GroupEntity u where u.name = :name and u.realmId = :realmId"), + @NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realmId = :realmId"), + @NamedQuery(name="deleteGroupsByRealm", query="delete from GroupEntity u where u.realmId = :realmId") +}) +@Entity +@Table(name="GROUP_ENTITY") +public class GroupEntity { + @Id + @Column(name="ID", length = 36) + protected String id; + + @Column(name = "NAME") + protected String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PARENT_GROUP") + private GroupEntity parent; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "REALM") + private RealmEntity realm; + + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="group") + protected Collection attributes = new ArrayList(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Collection getAttributes() { + return attributes; + } + + public void setAttributes(Collection attributes) { + this.attributes = attributes; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RealmEntity getRealm() { + return realm; + } + + public void setRealm(RealmEntity realm) { + this.realm = realm; + } + + public GroupEntity getParent() { + return parent; + } + + public void setParent(GroupEntity parent) { + this.parent = parent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GroupEntity that = (GroupEntity) o; + + if (!id.equals(that.id)) return false; + + return true; + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java new file mode 100755 index 0000000000..86db554f71 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java @@ -0,0 +1,101 @@ +package org.keycloak.models.jpa.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@NamedQueries({ + @NamedQuery(name="groupHasRole", query="select m from GroupRoleMappingEntity m where m.group = :group and m.roleId = :roleId"), + @NamedQuery(name="groupRoleMappings", query="select m from GroupRoleMappingEntity m where m.group = :group"), + @NamedQuery(name="groupRoleMappingIds", query="select m.roleId from GroupRoleMappingEntity m where m.group = :group"), + @NamedQuery(name="deleteGroupRoleMappingsByRealm", query="delete from GroupRoleMappingEntity mapping where mapping.group IN (select u from GroupEntity u where u.realmId=:realmId)"), + @NamedQuery(name="deleteGroupRoleMappingsByRole", query="delete from GroupRoleMappingEntity m where m.roleId = :roleId"), + @NamedQuery(name="deleteGroupRoleMappingsByGroup", query="delete from GroupRoleMappingEntity m where m.group = :group") + +}) +@Table(name="GROUP_ROLE_MAPPING") +@Entity +@IdClass(GroupRoleMappingEntity.Key.class) +public class GroupRoleMappingEntity { + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name="GROUP_ID") + protected GroupEntity group; + + @Id + @Column(name = "ROLE_ID") + protected String roleId; + + public GroupEntity getGroup() { + return group; + } + + public void setGroup(GroupEntity group) { + this.group = group; + } + + public String getRoleId() { + return roleId; + } + + public void setRoleId(String roleId) { + this.roleId = roleId; + } + + + public static class Key implements Serializable { + + protected GroupEntity group; + + protected String roleId; + + public Key() { + } + + public Key(GroupEntity group, String roleId) { + this.group = group; + this.roleId = roleId; + } + + public GroupEntity getGroup() { + return group; + } + + public String getRoleId() { + return roleId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (!roleId.equals(key.roleId)) return false; + if (!group.equals(key.group)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = group.hashCode(); + result = 31 * result + roleId.hashCode(); + return result; + } + } +} 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 bf5b339577..95b75d979e 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 @@ -133,6 +133,9 @@ public class RealmEntity { @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection roles = new ArrayList(); + @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") + Collection groups = new ArrayList(); + @ElementCollection @MapKeyColumn(name="NAME") @Column(name="VALUE") @@ -718,5 +721,21 @@ public class RealmEntity { public void setClientAuthenticationFlow(String clientAuthenticationFlow) { this.clientAuthenticationFlow = clientAuthenticationFlow; } + + public Collection getGroups() { + return groups; + } + + public void setGroups(Collection groups) { + this.groups = groups; + } + + public void addGroup(GroupEntity group) { + if (groups == null) { + groups = new ArrayList(); + } + groups.add(group); + } + } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java new file mode 100755 index 0000000000..76b7797dd0 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java @@ -0,0 +1,102 @@ +package org.keycloak.models.jpa.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@NamedQueries({ + @NamedQuery(name="userMemberOf", query="select m from UserGroupMembershipEntity m where m.user = :user and m.groupId = :groupId"), + @NamedQuery(name="userGroupMembership", query="select m from UserGroupMembershipEntity m where m.user = :user"), + @NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId"), + @NamedQuery(name="userGroupIds", query="select m.groupId from UserGroupMembershipEntity m where m.user = :user"), + @NamedQuery(name="deleteUserGroupMembershipByRealm", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"), + @NamedQuery(name="deleteUserGroupMembershipsByRealmAndLink", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), + @NamedQuery(name="deleteUserGroupMembershipsByGroup", query="delete from UserGroupMembershipEntity m where m.groupId = :groupId"), + @NamedQuery(name="deleteUserGroupMembershipsByUser", query="delete from UserGroupMembershipEntity m where m.user = :user") + +}) +@Table(name="USER_GROUP_MEMBERSHIP") +@Entity +@IdClass(UserGroupMembershipEntity.Key.class) +public class UserGroupMembershipEntity { + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name="USER_ID") + protected UserEntity user; + + @Id + @Column(name = "GROUP_ID") + protected String groupId; + + public UserEntity getUser() { + return user; + } + + public void setUser(UserEntity user) { + this.user = user; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public static class Key implements Serializable { + + protected UserEntity user; + + protected String groupId; + + public Key() { + } + + public Key(UserEntity user, String groupId) { + this.user = user; + this.groupId = groupId; + } + + public UserEntity getUser() { + return user; + } + + public String getGroupId() { + return groupId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (!groupId.equals(key.groupId)) return false; + if (!user.equals(key.user)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = user.hashCode(); + result = 31 * result + groupId.hashCode(); + return result; + } + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java new file mode 100755 index 0000000000..6fdb97b505 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java @@ -0,0 +1,225 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; +import org.keycloak.models.mongo.utils.MongoModelUtils; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author Marek Posolda + */ +public class GroupAdapter extends AbstractMongoAdapter implements GroupModel { + + private final MongoGroupEntity group; + private RealmModel realm; + private KeycloakSession session; + + public GroupAdapter(KeycloakSession session, RealmModel realm, MongoGroupEntity group, MongoStoreInvocationContext invContext) { + super(invContext); + this.group = group; + this.realm = realm; + this.session = session; + } + + @Override + public String getId() { + return group.getId(); + } + + @Override + public String getName() { + return group.getName(); + } + + @Override + public void setName(String name) { + group.setName(name); + updateGroup(); + } + + protected void updateGroup() { + super.updateMongoEntity(); + } + + @Override + public MongoGroupEntity getMongoEntity() { + return group; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof GroupModel)) return false; + + GroupModel that = (GroupModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public void setSingleAttribute(String name, String value) { + if (group.getAttributes() == null) { + group.setAttributes(new HashMap>()); + } + + List attrValues = new ArrayList<>(); + attrValues.add(value); + group.getAttributes().put(name, attrValues); + updateGroup(); + } + + @Override + public void setAttribute(String name, List values) { + if (group.getAttributes() == null) { + group.setAttributes(new HashMap>()); + } + + group.getAttributes().put(name, values); + updateGroup(); + } + + @Override + public void removeAttribute(String name) { + if (group.getAttributes() == null) return; + + group.getAttributes().remove(name); + updateGroup(); + } + + @Override + public String getFirstAttribute(String name) { + if (group.getAttributes()==null) return null; + + List attrValues = group.getAttributes().get(name); + return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0); + } + + @Override + public List getAttribute(String name) { + if (group.getAttributes()==null) return Collections.emptyList(); + List attrValues = group.getAttributes().get(name); + return (attrValues == null) ? Collections.emptyList() : Collections.unmodifiableList(attrValues); + } + + @Override + public Map> getAttributes() { + return group.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map) group.getAttributes()); + } + + @Override + public boolean hasRole(RoleModel role) { + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); + } + + @Override + public void grantRole(RoleModel role) { + getMongoStore().pushItemToList(group, "roleIds", role.getId(), true, invocationContext); + } + + @Override + public Set getRoleMappings() { + if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET; + Set roles = new HashSet<>(); + for (String id : group.getRoleIds()) { + roles.add(realm.getRoleById(id)); + } + return roles; + } + + @Override + public Set getRealmRoleMappings() { + Set allRoles = getRoleMappings(); + + // Filter to retrieve just realm roles + Set realmRoles = new HashSet(); + for (RoleModel role : allRoles) { + if (role.getContainer() instanceof RealmModel) { + realmRoles.add(role); + } + } + return realmRoles; + } + + @Override + public void deleteRoleMapping(RoleModel role) { + if (group == null || role == null) return; + + getMongoStore().pullItemFromList(group, "roleIds", role.getId(), invocationContext); + } + + @Override + public Set getClientRoleMappings(ClientModel app) { + Set result = new HashSet(); + Set roles = getRoleMappings(); + + for (RoleModel role : roles) { + if (app.equals(role.getContainer())) { + result.add(role); + } + } + return result; + } + + @Override + public GroupModel getParent() { + if (group.getParentId() == null) return null; + return realm.getGroupById(group.getParentId()); + } + + @Override + public Set getSubGroups() { + Set subGroups = new HashSet<>(); + for (GroupModel groupModel : realm.getGroups()) { + if (groupModel.getParent().equals(this)) { + subGroups.add(groupModel); + } + } + return subGroups; + } + + @Override + public void setParent(GroupModel group) { + this.group.setParentId(group.getId()); + updateGroup(); + + } + + @Override + public void addChild(GroupModel subGroup) { + subGroup.setParent(this); + updateGroup(); + + } + + @Override + public void removeChild(GroupModel subGroup) { + subGroup.setParent(null); + updateGroup(); + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java index a84026132e..18acfa9cc6 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java @@ -7,11 +7,13 @@ import org.keycloak.connections.mongo.api.MongoStore; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.migration.MigrationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity; import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; @@ -121,6 +123,14 @@ public class MongoRealmProvider implements RealmProvider { return new RoleAdapter(session, realm, role, null, invocationContext); } + @Override + public GroupModel getGroupById(String id, RealmModel realm) { + MongoGroupEntity group = getMongoStore().loadEntity(MongoGroupEntity.class, id, invocationContext); + if (group == null) return null; + if (group.getRealmId() != null && !group.getRealmId().equals(realm.getId())) return null; + return new GroupAdapter(session, realm, group, invocationContext); + } + @Override public ClientModel getClientById(String id, RealmModel realm) { MongoClientEntity appData = getMongoStore().loadEntity(MongoClientEntity.class, id, invocationContext); 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 358e6f2d28..bb4ab4c4dd 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 @@ -9,6 +9,7 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -90,10 +91,26 @@ public class MongoUserProvider implements UserProvider { } } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + QueryBuilder queryBuilder = new QueryBuilder() + .and("realmId").is(realm.getId()); + queryBuilder.and("groupIds").is(group.getId()); + DBObject sort = new BasicDBObject("username", 1); + + List users = getMongoStore().loadEntities(MongoUserEntity.class, queryBuilder.get(), sort, firstResult, maxResults, invocationContext); + return convertUserEntities(realm, users); + } + protected MongoStore getMongoStore() { return invocationContext.getMongoStore(); } + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return getGroupMembers(realm, group, -1, -1); + } + @Override public UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) { DBObject query = new QueryBuilder() @@ -411,6 +428,17 @@ public class MongoUserProvider implements UserProvider { getMongoStore().updateEntities(MongoUserConsentEntity.class, query, pull, invocationContext); } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + // Remove this role from all users, which has it + DBObject query = new QueryBuilder() + .and("groupIds").is(group.getId()) + .get(); + + DBObject pull = new BasicDBObject("$pull", query); + getMongoStore().updateEntities(MongoUserEntity.class, query, pull, invocationContext); + } + @Override public void preRemove(RealmModel realm, RoleModel role) { // Remove this role from all users, which has it 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 f21744de14..064b4617e8 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 @@ -9,6 +9,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -34,6 +35,7 @@ import org.keycloak.models.entities.RequiredCredentialEntity; import org.keycloak.models.entities.UserFederationMapperEntity; import org.keycloak.models.entities.UserFederationProviderEntity; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.utils.KeycloakModelUtils; @@ -607,6 +609,47 @@ public class RealmAdapter extends AbstractMongoAdapter impleme return model.getRoleById(id, this); } + @Override + public GroupModel getGroupById(String id) { + return model.getGroupById(id, this); + } + + @Override + public List getGroups() { + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + List groups = getMongoStore().loadEntities(MongoGroupEntity.class, query, invocationContext); + + List result = new LinkedList<>(); + + if (groups == null) return result; + for (MongoGroupEntity group : groups) { + result.add(new GroupAdapter(session, this, group, invocationContext)); + } + + return result; + } + + @Override + public List getTopLevelGroups() { + List all = getGroups(); + Iterator it = all.iterator(); + while (it.hasNext()) { + GroupModel group = it.next(); + if (group.getParent() != null) { + it.remove(); + } + } + return all; + } + + @Override + public boolean removeGroup(GroupModel group) { + session.users().preRemove(this, group); + return getMongoStore().removeEntity(MongoGroupEntity.class, group.getId(), invocationContext); + } + @Override public List getDefaultRoles() { return realm.getDefaultRoles(); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 3235eac1cc..87729d4a6b 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -7,6 +7,7 @@ import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.OTPPolicy; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserConsentModel; @@ -450,6 +451,37 @@ public class UserAdapter extends AbstractMongoAdapter implement return user; } + @Override + public Set getGroups() { + if (user.getGroupIds() == null && user.getGroupIds().size() == 0) return Collections.EMPTY_SET; + Set groups = new HashSet<>(); + for (String id : user.getGroupIds()) { + groups.add(realm.getGroupById(id)); + } + return groups; + } + + @Override + public void joinGroup(GroupModel group) { + getMongoStore().pushItemToList(getUser(), "groupIds", group.getId(), true, invocationContext); + + } + + @Override + public void leaveGroup(GroupModel group) { + if (user == null || group == null) return; + + getMongoStore().pullItemFromList(getUser(), "groupIds", group.getId(), invocationContext); + + } + + @Override + public boolean isMemberOf(GroupModel group) { + if (user.getGroupIds().contains(group.getId())) return true; + Set groups = getGroups(); + return KeycloakModelUtils.isMember(groups, group); + } + @Override public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoGroupEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoGroupEntity.java new file mode 100755 index 0000000000..80e51a92f1 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoGroupEntity.java @@ -0,0 +1,26 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.jboss.logging.Logger; +import org.keycloak.connections.mongo.api.MongoCollection; +import org.keycloak.connections.mongo.api.MongoField; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.MongoStore; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.GroupEntity; +import org.keycloak.models.entities.RoleEntity; + +import java.util.List; + +/** + */ +@MongoCollection(collectionName = "groups") +public class MongoGroupEntity extends GroupEntity implements MongoIdentifiableEntity { + + private static final Logger logger = Logger.getLogger(MongoGroupEntity.class); + + @Override + public void afterRemove(MongoStoreInvocationContext invContext) { + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java index 9c2b326005..eaf221698a 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java @@ -4,11 +4,13 @@ import com.mongodb.DBObject; import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.mongo.keycloak.adapters.ClientAdapter; +import org.keycloak.models.mongo.keycloak.adapters.GroupAdapter; import org.keycloak.models.mongo.keycloak.adapters.UserAdapter; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java index b501da9e31..51dbbd4f45 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite; import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -67,6 +68,11 @@ public class DummyUserFederationProvider implements UserFederationProvider { } + @Override + public void preRemove(RealmModel realm, GroupModel group) { + + } + @Override public boolean isValid(RealmModel realm, UserModel local) { return false;