diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java index e1b57680ea..39cac56441 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java @@ -44,6 +44,13 @@ import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.user.MapUserConsentEntity; +import org.keycloak.models.map.user.MapUserConsentEntityImpl; +import org.keycloak.models.map.user.MapUserCredentialEntity; +import org.keycloak.models.map.user.MapUserCredentialEntityImpl; +import org.keycloak.models.map.user.MapUserEntityImpl; +import org.keycloak.models.map.user.MapUserFederatedIdentityEntity; +import org.keycloak.models.map.user.MapUserFederatedIdentityEntityImpl; import org.keycloak.models.map.storage.ModelEntityUtil; import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderConfigProperty; @@ -80,10 +87,14 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide private final static DeepCloner CLONER = new DeepCloner.Builder() .genericCloner(Serialization::from) - .constructor(MapClientEntityImpl.class, MapClientEntityImpl::new) - .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) - .constructor(MapGroupEntityImpl.class, MapGroupEntityImpl::new) - .constructor(MapRoleEntityImpl.class, MapRoleEntityImpl::new) + .constructor(MapClientEntityImpl.class, MapClientEntityImpl::new) + .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) + .constructor(MapGroupEntityImpl.class, MapGroupEntityImpl::new) + .constructor(MapRoleEntityImpl.class, MapRoleEntityImpl::new) + .constructor(MapUserEntityImpl.class, MapUserEntityImpl::new) + .constructor(MapUserCredentialEntityImpl.class, MapUserCredentialEntityImpl::new) + .constructor(MapUserFederatedIdentityEntityImpl.class, MapUserFederatedIdentityEntityImpl::new) + .constructor(MapUserConsentEntityImpl.class, MapUserConsentEntityImpl::new) .build(); private static final Map KEY_CONVERTORS = new HashMap<>(); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index 1aac3c7a3b..03a1e14afc 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -44,6 +44,7 @@ import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; import org.keycloak.models.map.realm.MapRealmEntity; import org.keycloak.models.map.role.MapRoleEntity; import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.user.MapUserConsentEntity; import org.keycloak.storage.SearchableModelField; import java.util.Comparator; @@ -52,7 +53,6 @@ import java.util.Map; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.user.MapUserEntity; -import org.keycloak.models.map.user.UserConsentEntity; import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity; import org.keycloak.models.map.userSession.MapUserSessionEntity; import org.keycloak.sessions.RootAuthenticationSessionModel; @@ -279,7 +279,7 @@ public class MapFieldPredicates { String providerId = ensureEqSingleValue(UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK, "provider_id", op, values); String providerIdS = new StorageId((String) providerId, "").getId(); Function getter; - getter = ue -> ue.getUserConsents().map(UserConsentEntity::getClientId).anyMatch(v -> v != null && v.startsWith(providerIdS)); + getter = ue -> Optional.ofNullable(ue.getUserConsents()).orElseGet(Collections::emptyMap).values().stream().map(MapUserConsentEntity::getClientId).anyMatch(v -> v != null && v.startsWith(providerIdS)); return mcb.fieldCompare(Boolean.TRUE::equals, getter); } @@ -330,7 +330,7 @@ public class MapFieldPredicates { private static MapModelCriteriaBuilder checkGrantedUserRole(MapModelCriteriaBuilder mcb, Operator op, Object[] values) { String roleIdS = ensureEqSingleValue(UserModel.SearchableFields.ASSIGNED_ROLE, "role_id", op, values); Function getter; - getter = ue -> ue.getRolesMembership().contains(roleIdS); + getter = ue -> Optional.ofNullable(ue.getRolesMembership()).orElseGet(Collections::emptySet).contains(roleIdS); return mcb.fieldCompare(Boolean.TRUE::equals, getter); } @@ -433,10 +433,10 @@ public class MapFieldPredicates { Function getter; if (op == Operator.IN && values != null && values.length == 1 && (values[0] instanceof Collection)) { Collection c = (Collection) values[0]; - getter = ue -> ue.getGroupsMembership().stream().anyMatch(c::contains); + getter = ue -> Optional.ofNullable(ue.getGroupsMembership()).orElseGet(Collections::emptySet).stream().anyMatch(c::contains); } else { String groupIdS = ensureEqSingleValue(UserModel.SearchableFields.ASSIGNED_GROUP, "group_id", op, values); - getter = ue -> ue.getGroupsMembership().contains(groupIdS); + getter = ue -> Optional.ofNullable(ue.getGroupsMembership()).orElseGet(Collections::emptySet).contains(groupIdS); } return mcb.fieldCompare(Boolean.TRUE::equals, getter); @@ -453,7 +453,7 @@ public class MapFieldPredicates { private static MapModelCriteriaBuilder checkUserConsentsWithClientScope(MapModelCriteriaBuilder mcb, Operator op, Object[] values) { String clientScopeIdS = ensureEqSingleValue(UserModel.SearchableFields.CONSENT_FOR_CLIENT, "client_scope_id", op, values); Function getter; - getter = ue -> ue.getUserConsents().anyMatch(consent -> consent.getGrantedClientScopesIds().contains(clientScopeIdS)); + getter = ue -> Optional.ofNullable(ue.getUserConsents()).orElseGet(Collections::emptyMap).values().stream().anyMatch(consent -> Optional.ofNullable(consent.getGrantedClientScopesIds()).orElseGet(Collections::emptySet).contains(clientScopeIdS)); return mcb.fieldCompare(Boolean.TRUE::equals, getter); } @@ -469,15 +469,15 @@ public class MapFieldPredicates { final Object idpAlias = values[0]; Function getter; if (values.length == 1) { - getter = ue -> ue.getFederatedIdentities() + getter = ue -> Optional.ofNullable(ue.getFederatedIdentities()).orElseGet(Collections::emptyMap).values().stream() .anyMatch(aue -> Objects.equals(idpAlias, aue.getIdentityProvider())); } else if (idpAlias == null) { final Object idpUserId = values[1]; - getter = ue -> ue.getFederatedIdentities() + getter = ue -> Optional.ofNullable(ue.getFederatedIdentities()).orElseGet(Collections::emptyMap).values().stream() .anyMatch(aue -> Objects.equals(idpUserId, aue.getUserId())); } else { final Object idpUserId = values[1]; - getter = ue -> ue.getFederatedIdentities() + getter = ue -> Optional.ofNullable(ue.getFederatedIdentities()).orElseGet(Collections::emptyMap).values().stream() .anyMatch(aue -> Objects.equals(idpAlias, aue.getIdentityProvider()) && Objects.equals(idpUserId, aue.getUserId())); } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java index 54f31fc655..24eeb3739e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; @@ -76,7 +77,8 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public boolean isEnabled() { - return entity.isEnabled(); + Boolean enabled = entity.isEnabled(); + return enabled != null && enabled; } @Override @@ -147,19 +149,20 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public String getFirstAttribute(String name) { return getSpecialAttributeValue(name) - .orElseGet(() -> entity.getAttribute(name).stream().findFirst() + .orElseGet(() -> Optional.ofNullable(entity.getAttribute(name)).orElseGet(Collections::emptyList).stream().findFirst() .orElse(null)); } @Override public Stream getAttributeStream(String name) { return getSpecialAttributeValue(name).map(Collections::singletonList) - .orElseGet(() -> entity.getAttribute(name)).stream(); + .orElseGet(() -> Optional.ofNullable(entity.getAttribute(name)).orElseGet(Collections::emptyList)).stream(); } @Override public Map> getAttributes() { - MultivaluedHashMap result = new MultivaluedHashMap<>(entity.getAttributes()); + Map> attributes = entity.getAttributes(); + MultivaluedHashMap result = attributes == null ? new MultivaluedHashMap<>() : new MultivaluedHashMap<>(attributes); result.add(UserModel.FIRST_NAME, entity.getFirstName()); result.add(UserModel.LAST_NAME, entity.getLastName()); result.add(UserModel.EMAIL, entity.getEmail()); @@ -170,7 +173,8 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public Stream getRequiredActionsStream() { - return entity.getRequiredActions().stream(); + Set requiredActions = entity.getRequiredActions(); + return requiredActions == null ? Stream.empty() : requiredActions.stream(); } @Override @@ -233,7 +237,8 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public boolean isEmailVerified() { - return entity.isEmailVerified(); + Boolean emailVerified = entity.isEmailVerified(); + return emailVerified != null && emailVerified; } @Override @@ -243,7 +248,9 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public Stream getGroupsStream() { - return session.groups().getGroupsStream(realm, entity.getGroupsMembership().stream()); + Set groups = entity.getGroupsMembership(); + if (groups == null || groups.isEmpty()) return Stream.empty(); + return session.groups().getGroupsStream(realm, groups.stream()); } @Override @@ -258,7 +265,8 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public boolean isMemberOf(GroupModel group) { - return entity.getGroupsMembership().contains(group.getId()); + Set groups = entity.getGroupsMembership(); + return groups != null && groups.contains(group.getId()); } @Override @@ -294,7 +302,8 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public boolean hasDirectRole(RoleModel role) { - return entity.getRolesMembership().contains(role.getId()); + Set roles = entity.getRolesMembership(); + return roles != null && entity.getRolesMembership().contains(role.getId()); } @Override @@ -309,6 +318,8 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public Stream getRoleMappingsStream() { + Set roles = entity.getRolesMembership(); + if (roles == null || roles.isEmpty()) return Stream.empty(); return entity.getRolesMembership().stream().map(realm::getRoleById); } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserConsentEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserConsentEntity.java new file mode 100644 index 0000000000..46e9138b10 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserConsentEntity.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserConsentModel; +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.Objects; +import java.util.Set; + +@GenerateEntityImplementations +@DeepCloner.Root +public interface MapUserConsentEntity extends UpdatableEntity { + + public static MapUserConsentEntity fromModel(UserConsentModel model) { + long currentTime = Time.currentTimeMillis(); + + MapUserConsentEntity consentEntity = new MapUserConsentEntityImpl(); + consentEntity.setClientId(model.getClient().getId()); + consentEntity.setCreatedDate(currentTime); + consentEntity.setLastUpdatedDate(currentTime); + + model.getGrantedClientScopes() + .stream() + .map(ClientScopeModel::getId) + .forEach(consentEntity::addGrantedClientScopesId); + + return consentEntity; + } + + public static UserConsentModel toModel(RealmModel realm, MapUserConsentEntity entity) { + if (entity == null) { + return null; + } + + ClientModel client = realm.getClientById(entity.getClientId()); + if (client == null) { + throw new ModelException("Client with id " + entity.getClientId() + " is not available"); + } + UserConsentModel model = new UserConsentModel(client); + model.setCreatedDate(entity.getCreatedDate()); + model.setLastUpdatedDate(entity.getLastUpdatedDate()); + + + Set grantedClientScopesIds = entity.getGrantedClientScopesIds(); + + if (grantedClientScopesIds != null && !grantedClientScopesIds.isEmpty()) { + grantedClientScopesIds.stream() + .map(scopeId -> KeycloakModelUtils.findClientScopeById(realm, client, scopeId)) + .filter(Objects::nonNull) + .forEach(model::addGrantedClientScope); + } + + return model; + } + + String getClientId(); + void setClientId(String clientId); + + Set getGrantedClientScopesIds(); + void addGrantedClientScopesId(String scope); + void setGrantedClientScopesIds(Set scopesIds); + void removeGrantedClientScopesId(String scopesId); + + Long getCreatedDate(); + void setCreatedDate(Long createdDate); + + Long getLastUpdatedDate(); + void setLastUpdatedDate(Long lastUpdatedDate); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserCredentialEntity.java new file mode 100644 index 0000000000..d6dd6e0347 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserCredentialEntity.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.credential.CredentialModel; +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.Comparator; + +@GenerateEntityImplementations +@DeepCloner.Root +public interface MapUserCredentialEntity extends UpdatableEntity { + + Comparator ORDER_BY_PRIORITY = Comparator.comparing(MapUserCredentialEntity::getPriority); + + public static MapUserCredentialEntity fromModel(CredentialModel model) { + MapUserCredentialEntity credentialEntity = new MapUserCredentialEntityImpl(); + String id = model.getId() == null ? KeycloakModelUtils.generateId() : model.getId(); + credentialEntity.setId(id); + credentialEntity.setCreatedDate(model.getCreatedDate()); + credentialEntity.setUserLabel(model.getUserLabel()); + credentialEntity.setType(model.getType()); + credentialEntity.setSecretData(model.getSecretData()); + credentialEntity.setCredentialData(model.getCredentialData()); + + return credentialEntity; + } + + public static CredentialModel toModel(MapUserCredentialEntity entity) { + CredentialModel model = new CredentialModel(); + model.setId(entity.getId()); + model.setType(entity.getType()); + model.setCreatedDate(entity.getCreatedDate()); + model.setUserLabel(entity.getUserLabel()); + model.setSecretData(entity.getSecretData()); + model.setCredentialData(entity.getCredentialData()); + return model; + } + + String getId(); + void setId(String id); + + String getType(); + void setType(String type); + + String getUserLabel(); + void setUserLabel(String userLabel); + + Long getCreatedDate(); + void setCreatedDate(Long createdDate); + + String getSecretData(); + void setSecretData(String secretData); + + String getCredentialData(); + void setCredentialData(String credentialData); + + Integer getPriority(); + void setPriority(Integer priority); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java index 3d82273545..655918af31 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java @@ -1,13 +1,13 @@ /* - * Copyright 2020 Red Hat, Inc. and/or its affiliates + * Copyright 2021 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,363 +17,140 @@ package org.keycloak.models.map.user; -import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.EntityWithAttributes; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; -/** - * - * @author mhajas - */ -public class MapUserEntity extends UpdatableEntity.Impl implements AbstractEntity, EntityWithAttributes { +@GenerateEntityImplementations( + inherits = "org.keycloak.models.map.user.MapUserEntity.AbstractUserEntity" +) +@DeepCloner.Root +public interface MapUserEntity extends UpdatableEntity, AbstractEntity, EntityWithAttributes { - private String id; - private String realmId; + public abstract class AbstractUserEntity extends UpdatableEntity.Impl implements MapUserEntity { - private String username; - private String firstName; - private Long createdTimestamp; - private String lastName; - private String email; - private boolean enabled; - private boolean emailVerified; - // This is necessary to be able to dynamically switch unique email constraints on and off in the realm settings - private String emailConstraint = KeycloakModelUtils.generateId(); - private Map> attributes = new HashMap<>(); - private Set requiredActions = new HashSet<>(); - private final Map credentials = new HashMap<>(); - private final List credentialsOrder = new LinkedList<>(); - private final Map federatedIdentities = new HashMap<>(); - private final Map userConsents = new HashMap<>(); - private Set groupsMembership = new HashSet<>(); - private Set rolesMembership = new HashSet<>(); - private String federationLink; - private String serviceAccountClientLink; - private int notBefore; + private String id; - /** - * Flag signalizing that any of the setters has been meaningfully used. - */ - - public MapUserEntity() {} - - public MapUserEntity(String id, String realmId) { - this.id = id; - this.realmId = realmId; - } - - @Override - public String getId() { - return this.id; - } - - @Override - public void setId(String id) { - if (this.id != null) throw new IllegalStateException("Id cannot be changed"); - this.id = id; - this.updated |= id != null; - } - - @Override - public boolean isUpdated() { - return this.updated - || userConsents.values().stream().anyMatch(UserConsentEntity::isUpdated) - || credentials.values().stream().anyMatch(UserCredentialEntity::isUpdated) - || federatedIdentities.values().stream().anyMatch(UserFederatedIdentityEntity::isUpdated); - } - - public String getRealmId() { - return realmId; - } - - public void setRealmId(String realmId) { - this.updated |= !Objects.equals(this.realmId, realmId); - this.realmId = realmId; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.updated |= !Objects.equals(this.username, username); - this.username = username; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.updated |= !Objects.equals(this.firstName, firstName); - this.firstName = firstName; - } - - public Long getCreatedTimestamp() { - return createdTimestamp; - } - - public void setCreatedTimestamp(Long createdTimestamp) { - this.updated |= !Objects.equals(this.createdTimestamp, createdTimestamp); - this.createdTimestamp = createdTimestamp; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.updated |= !Objects.equals(this.lastName, lastName); - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email, boolean duplicateEmailsAllowed) { - this.updated |= !Objects.equals(this.email, email); - this.email = email; - this.emailConstraint = email == null || duplicateEmailsAllowed ? KeycloakModelUtils.generateId() : email; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.updated |= !Objects.equals(this.enabled, enabled); - this.enabled = enabled; - } - - public boolean isEmailVerified() { - return emailVerified; - } - - public void setEmailVerified(boolean emailVerified) { - this.updated |= !Objects.equals(this.emailVerified, emailVerified); - this.emailVerified = emailVerified; - } - - public String getEmailConstraint() { - return emailConstraint; - } - - public void setEmailConstraint(String emailConstraint) { - this.updated |= !Objects.equals(this.emailConstraint, emailConstraint); - this.emailConstraint = emailConstraint; - } - - public Map> getAttributes() { - return attributes; - } - - @Override - public List getAttribute(String name) { - return attributes.getOrDefault(name, Collections.emptyList()); - } - - @Override - public void setAttributes(Map> attributes) { - this.updated |= !Objects.equals(this.attributes, attributes); - this.attributes = attributes; - } - - @Override - public void setAttribute(String name, List value) { - this.updated |= !Objects.equals(this.attributes.put(name, value), value); - } - - @Override - public void removeAttribute(String name) { - this.updated |= this.attributes.remove(name) != null; - } - - public Set getRequiredActions() { - return requiredActions; - } - - public void setRequiredActions(Set requiredActions) { - this.updated |= !Objects.equals(this.requiredActions, requiredActions); - this.requiredActions = requiredActions; - } - - public void addRequiredAction(String requiredAction) { - this.updated |= this.requiredActions.add(requiredAction); - } - - public void removeRequiredAction(String requiredAction) { - this.updated |= this.requiredActions.remove(requiredAction); - } - - public void updateCredential(UserCredentialEntity credentialEntity) { - this.updated |= credentials.replace(credentialEntity.getId(), credentialEntity) != null; - } - - public void addCredential(UserCredentialEntity credentialEntity) { - if (credentials.containsKey(credentialEntity.getId())) { - throw new ModelDuplicateException("A CredentialModel with given id already exists"); + @Override + public boolean isUpdated() { + return this.updated + || Optional.ofNullable(getUserConsents()).orElseGet(Collections::emptyMap).values().stream().anyMatch(MapUserConsentEntity::isUpdated) + || Optional.ofNullable(getCredentials()).orElseGet(Collections::emptyMap).values().stream().anyMatch(MapUserCredentialEntity::isUpdated) + || Optional.ofNullable(getFederatedIdentities()).orElseGet(Collections::emptyMap).values().stream().anyMatch(MapUserFederatedIdentityEntity::isUpdated); } - this.updated = true; - credentials.put(credentialEntity.getId(), credentialEntity); - credentialsOrder.add(credentialEntity.getId()); - } - - public boolean removeCredential(String credentialId) { - if (!credentials.containsKey(credentialId)) { - return false; + @Override + public void clearUpdatedFlag() { + this.updated = false; + Optional.ofNullable(getUserConsents()).orElseGet(Collections::emptyMap).values().forEach(UpdatableEntity::clearUpdatedFlag); + Optional.ofNullable(getCredentials()).orElseGet(Collections::emptyMap).values().forEach(UpdatableEntity::clearUpdatedFlag); + Optional.ofNullable(getFederatedIdentities()).orElseGet(Collections::emptyMap).values().forEach(UpdatableEntity::clearUpdatedFlag); + } - this.updated = true; - this.credentials.remove(credentialId); - this.credentialsOrder.remove(credentialId); + @Override + public String getId() { + return this.id; + } - return true; - } - - public UserCredentialEntity getCredential(String id) { - return credentials.get(id); - } - - public Stream getCredentials() { - return credentialsOrder.stream() - .map(credentials::get); - } - - public int getCredentialIndex(String credentialId) { - return credentialsOrder.indexOf(credentialId); - } - - public void moveCredential(int currentPosition, int newPosition) { - this.updated |= currentPosition != newPosition; - credentialsOrder.add(newPosition, credentialsOrder.remove(currentPosition)); + @Override + public void setId(String id) { + if (this.id != null) throw new IllegalStateException("Id cannot be changed"); + this.id = id; + this.updated |= id != null; + } + + @Override + public void setEmail(String email, boolean duplicateEmailsAllowed) { + this.setEmail(email); + this.setEmailConstraint(email == null || duplicateEmailsAllowed ? KeycloakModelUtils.generateId() : email); + } } - public Stream getFederatedIdentities() { - return federatedIdentities.values().stream(); - } + String getRealmId(); + void setRealmId(String realmId); - public void setFederatedIdentities(Collection federatedIdentities) { - this.updated = true; - this.federatedIdentities.clear(); - this.federatedIdentities.putAll(federatedIdentities.stream() - .collect(Collectors.toMap(UserFederatedIdentityEntity::getIdentityProvider, Function.identity()))); - } - - public void addFederatedIdentity(UserFederatedIdentityEntity federatedIdentity) { - String idpId = federatedIdentity.getIdentityProvider(); - this.updated |= !Objects.equals(this.federatedIdentities.put(idpId, federatedIdentity), federatedIdentity); - } + String getUsername(); + void setUsername(String username); - public UserFederatedIdentityEntity getFederatedIdentity(String federatedIdentity) { - return this.federatedIdentities.get(federatedIdentity); - } - - public boolean removeFederatedIdentity(String providerId) { - boolean removed = federatedIdentities.remove(providerId) != null; - this.updated |= removed; - return removed; - } + String getFirstName(); + void setFirstName(String firstName); - public void updateFederatedIdentity(UserFederatedIdentityEntity federatedIdentityModel) { - this.updated |= federatedIdentities.replace(federatedIdentityModel.getIdentityProvider(), federatedIdentityModel) != null; - } + Long getCreatedTimestamp(); + void setCreatedTimestamp(Long createdTimestamp); - public Stream getUserConsents() { - return userConsents.values().stream(); - } + String getLastName(); + void setLastName(String lastName); - public UserConsentEntity getUserConsent(String clientId) { - return this.userConsents.get(clientId); - } + String getEmail(); + void setEmail(String email); + @IgnoreForEntityImplementationGenerator + void setEmail(String email, boolean duplicateEmailsAllowed); - - public void addUserConsent(UserConsentEntity userConsentEntity) { - String clientId = userConsentEntity.getClientId(); - this.updated |= !Objects.equals(this.userConsents.put(clientId, userConsentEntity), userConsentEntity); - } + Boolean isEnabled(); + void setEnabled(Boolean enabled); - public boolean removeUserConsent(String clientId) { - boolean removed = userConsents.remove(clientId) != null; - this.updated |= removed; - return removed; - } + Boolean isEmailVerified(); + void setEmailVerified(Boolean emailVerified); - public Set getGroupsMembership() { - return groupsMembership; - } + String getEmailConstraint(); + void setEmailConstraint(String emailConstraint); - public void setGroupsMembership(Set groupsMembership) { - this.updated |= Objects.equals(groupsMembership, this.groupsMembership); - this.groupsMembership = groupsMembership; - } - - public void addGroupsMembership(String groupId) { - this.updated |= this.groupsMembership.add(groupId); - } + Map> getAttributes(); + List getAttribute(String name); + void setAttributes(Map> attributes); + void setAttribute(String name, List value); + void removeAttribute(String name); - public void removeGroupsMembership(String groupId) { - this.updated |= this.groupsMembership.remove(groupId); - } + Set getRequiredActions(); + void setRequiredActions(Set requiredActions); + void addRequiredAction(String requiredAction); + void removeRequiredAction(String requiredAction); - public Set getRolesMembership() { - return rolesMembership; - } + Map getCredentials(); + void setCredential(String id, MapUserCredentialEntity credentialEntity); + Boolean removeCredential(String credentialId); + MapUserCredentialEntity getCredential(String id); - public void setRolesMembership(Set rolesMembership) { - this.updated |= Objects.equals(rolesMembership, this.rolesMembership); - this.rolesMembership = rolesMembership; - } + Map getFederatedIdentities(); + void setFederatedIdentities(Map federatedIdentities); + void setFederatedIdentity(String id, MapUserFederatedIdentityEntity federatedIdentity); + MapUserFederatedIdentityEntity getFederatedIdentity(String federatedIdentity); + Boolean removeFederatedIdentity(String providerId); - public void addRolesMembership(String roleId) { - this.updated |= this.rolesMembership.add(roleId); - } + Map getUserConsents(); + MapUserConsentEntity getUserConsent(String clientId); + void setUserConsent(String id, MapUserConsentEntity userConsentEntity); + Boolean removeUserConsent(String clientId); - public void removeRolesMembership(String roleId) { - this.updated |= this.rolesMembership.remove(roleId); - } + Set getGroupsMembership(); + void setGroupsMembership(Set groupsMembership); + void addGroupsMembership(String groupId); + void removeGroupsMembership(String groupId); - public String getFederationLink() { - return federationLink; - } + Set getRolesMembership(); + void setRolesMembership(Set rolesMembership); + void addRolesMembership(String roleId); + void removeRolesMembership(String roleId); - public void setFederationLink(String federationLink) { - this.updated |= !Objects.equals(this.federationLink, federationLink); - this.federationLink = federationLink; - } + String getFederationLink(); + void setFederationLink(String federationLink); - public String getServiceAccountClientLink() { - return serviceAccountClientLink; - } - - public void setServiceAccountClientLink(String serviceAccountClientLink) { - this.updated |= !Objects.equals(this.serviceAccountClientLink, serviceAccountClientLink); - this.serviceAccountClientLink = serviceAccountClientLink; - } - - public int getNotBefore() { - return notBefore; - } - - public void setNotBefore(int notBefore) { - this.updated |= !Objects.equals(this.notBefore, notBefore); - this.notBefore = notBefore; - } + String getServiceAccountClientLink(); + void setServiceAccountClientLink(String serviceAccountClientLink); + Integer getNotBefore(); + void setNotBefore(Integer notBefore); } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserFederatedIdentityEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserFederatedIdentityEntity.java new file mode 100644 index 0000000000..6a148bd58a --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserFederatedIdentityEntity.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.UpdatableEntity; + +@GenerateEntityImplementations +@DeepCloner.Root +public interface MapUserFederatedIdentityEntity extends UpdatableEntity { + + public static MapUserFederatedIdentityEntity fromModel(FederatedIdentityModel model) { + if (model == null) return null; + MapUserFederatedIdentityEntity entity = new MapUserFederatedIdentityEntityImpl(); + entity.setIdentityProvider(model.getIdentityProvider()); + entity.setUserId(model.getUserId()); + entity.setUserName(model.getUserName().toLowerCase()); + entity.setToken(model.getToken()); + + return entity; + } + + public static FederatedIdentityModel toModel(MapUserFederatedIdentityEntity entity) { + if (entity == null) return null; + return new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName(), entity.getToken()); + } + + String getToken(); + void setToken(String token); + + String getUserId(); + void setUserId(String userId); + + String getIdentityProvider(); + void setIdentityProvider(String identityProvider); + + String getUserName(); + void setUserName(String userName); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java index df333b8daa..582125007d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -45,13 +45,17 @@ import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.client.ClientStorageProvider; +import java.util.Collection; +import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -75,6 +79,8 @@ import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.crit public class MapUserProvider implements UserProvider.Streams, UserCredentialStore.Streams { + // Typical priority difference between 2 credentials + public static final int PRIORITY_DIFFERENCE = 10; private static final Logger LOG = Logger.getLogger(MapUserProvider.class); private final KeycloakSession session; final MapKeycloakTransaction tx; @@ -139,15 +145,18 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor getEntityById(realm, user.getId()) .ifPresent(userEntity -> - userEntity.addFederatedIdentity(UserFederatedIdentityEntity.fromModel(socialLink))); + userEntity.setFederatedIdentity(socialLink.getIdentityProvider(), MapUserFederatedIdentityEntity.fromModel(socialLink))); } @Override public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) { LOG.tracef("removeFederatedIdentity(%s, %s, %s)%s", realm, user.getId(), socialProvider, getShortStackTrace()); - return getEntityById(realm, user.getId()) - .map(entity -> entity.removeFederatedIdentity(socialProvider)) - .orElse(false); + + Optional entityById = getEntityById(realm, user.getId()); + if (!entityById.isPresent()) return false; + + Boolean result = entityById.get().removeFederatedIdentity(socialProvider); + return result == null ? true : result; // TODO: make removeFederatedIdentity return Boolean so the caller can correctly handle "I don't know" null answer } @Override @@ -166,15 +175,18 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) { LOG.tracef("updateFederatedIdentity(%s, %s, %s)%s", realm, federatedUser.getId(), federatedIdentityModel.getIdentityProvider(), getShortStackTrace()); getEntityById(realm, federatedUser.getId()) - .ifPresent(entity -> entity.updateFederatedIdentity(UserFederatedIdentityEntity.fromModel(federatedIdentityModel))); + .ifPresent(entity -> entity.setFederatedIdentity(federatedIdentityModel.getIdentityProvider(), MapUserFederatedIdentityEntity.fromModel(federatedIdentityModel))); } @Override public Stream getFederatedIdentitiesStream(RealmModel realm, UserModel user) { LOG.tracef("getFederatedIdentitiesStream(%s, %s)%s", realm, user.getId(), getShortStackTrace()); return getEntityById(realm, user.getId()) - .map(MapUserEntity::getFederatedIdentities).orElseGet(Stream::empty) - .map(UserFederatedIdentityEntity::toModel); + .map(MapUserEntity::getFederatedIdentities) + .map(Map::values) + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(MapUserFederatedIdentityEntity::toModel); } @Override @@ -182,7 +194,7 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor LOG.tracef("getFederatedIdentity(%s, %s, %s)%s", realm, user.getId(), socialProvider, getShortStackTrace()); return getEntityById(realm, user.getId()) .map(userEntity -> userEntity.getFederatedIdentity(socialProvider)) - .map(UserFederatedIdentityEntity::toModel) + .map(MapUserFederatedIdentityEntity::toModel) .orElse(null); } @@ -213,7 +225,7 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor LOG.tracef("addConsent(%s, %s, %s)%s", realm, userId, consent, getShortStackTrace()); getEntityByIdOrThrow(realm, userId) - .addUserConsent(UserConsentEntity.fromModel(consent)); + .setUserConsent(consent.getClient().getId(), MapUserConsentEntity.fromModel(consent)); } @Override @@ -221,7 +233,7 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor LOG.tracef("getConsentByClient(%s, %s, %s)%s", realm, userId, clientInternalId, getShortStackTrace()); return getEntityById(realm, userId) .map(userEntity -> userEntity.getUserConsent(clientInternalId)) - .map(consent -> UserConsentEntity.toModel(realm, consent)) + .map(consent -> MapUserConsentEntity.toModel(realm, consent)) .orElse(null); } @@ -230,8 +242,10 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor LOG.tracef("getConsentByClientStream(%s, %s)%s", realm, userId, getShortStackTrace()); return getEntityById(realm, userId) .map(MapUserEntity::getUserConsents) + .map(Map::values) + .map(Collection::stream) .orElse(Stream.empty()) - .map(consent -> UserConsentEntity.toModel(realm, consent)); + .map(consent -> MapUserConsentEntity.toModel(realm, consent)); } @Override @@ -239,7 +253,7 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor LOG.tracef("updateConsent(%s, %s, %s)%s", realm, userId, consent, getShortStackTrace()); MapUserEntity user = getEntityByIdOrThrow(realm, userId); - UserConsentEntity userConsentEntity = user.getUserConsent(consent.getClient().getId()); + MapUserConsentEntity userConsentEntity = user.getUserConsent(consent.getClient().getId()); if (userConsentEntity == null) { throw new ModelException("Consent not found for client [" + consent.getClient().getId() + "] and user [" + userId + "]"); } @@ -256,9 +270,12 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor @Override public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) { LOG.tracef("revokeConsentForClient(%s, %s, %s)%s", realm, userId, clientInternalId, getShortStackTrace()); - return getEntityById(realm, userId) - .map(userEntity -> userEntity.removeUserConsent(clientInternalId)) - .orElse(false); + + Optional entityById = getEntityById(realm, userId); + if (!entityById.isPresent()) return false; + + Boolean result = entityById.get().removeUserConsent(clientInternalId); + return result == null ? true : result; // TODO: make revokeConsentForClient return Boolean so the caller can correctly handle "I don't know" null answer } @Override @@ -270,9 +287,11 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor @Override public int getNotBeforeOfUser(RealmModel realm, UserModel user) { LOG.tracef("getNotBeforeOfUser(%s, %s)%s", realm, user.getId(), getShortStackTrace()); - return getEntityById(realm, user.getId()) + Integer notBefore = getEntityById(realm, user.getId()) .orElseThrow(this::userDoesntExistException) .getNotBefore(); + + return notBefore == null ? 0 : notBefore; } @Override @@ -313,7 +332,10 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor throw new ModelDuplicateException("User exists: " + id); } - MapUserEntity entity = new MapUserEntity(id, realm.getId()); + MapUserEntity entity = new MapUserEntityImpl(); + entity.setId(id); + entity.setRealmId(realm.getId()); + entity.setEmailConstraint(KeycloakModelUtils.generateId()); entity.setUsername(username.toLowerCase()); entity.setCreatedTimestamp(Time.currentTimeMillis()); @@ -423,8 +445,11 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor .compare(SearchableFields.CONSENT_WITH_CLIENT_SCOPE, Operator.EQ, clientScopeId); try (Stream s = tx.read(withCriteria(mcb))) { - s.flatMap(MapUserEntity::getUserConsents) - .forEach(consent -> consent.removeGrantedClientScopesIds(clientScopeId)); + s.map(MapUserEntity::getUserConsents) + .filter(Objects::nonNull) + .map(Map::values) + .flatMap(Collection::stream) + .forEach(consent -> consent.removeGrantedClientScopesId(clientScopeId)); } } @@ -449,8 +474,10 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor private Consumer removeConsentsForExternalClient(String idPrefix) { return userEntity -> { - List consentClientIds = userEntity.getUserConsents() - .map(UserConsentEntity::getClientId) + Map userConsents = userEntity.getUserConsents(); + if (userConsents == null || userConsents.isEmpty()) return; + List consentClientIds = userConsents.values().stream() + .map(MapUserConsentEntity::getClientId) .filter(clientId -> clientId != null && clientId.startsWith(idPrefix)) .collect(Collectors.toList()); @@ -730,7 +757,7 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor private Consumer updateCredential(CredentialModel credentialModel) { return user -> { - UserCredentialEntity credentialEntity = user.getCredential(credentialModel.getId()); + MapUserCredentialEntity credentialEntity = user.getCredential(credentialModel.getId()); if (credentialEntity == null) return; credentialEntity.setCreatedDate(credentialModel.getCreatedDate()); @@ -744,20 +771,35 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor @Override public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { LOG.tracef("createCredential(%s, %s, %s)%s", realm, user.getId(), cred.getId(), getShortStackTrace()); - UserCredentialEntity credentialEntity = UserCredentialEntity.fromModel(cred); + MapUserEntity userEntity = getEntityByIdOrThrow(realm, user.getId()); + MapUserCredentialEntity credentialEntity = MapUserCredentialEntity.fromModel(cred); - getEntityByIdOrThrow(realm, user.getId()) - .addCredential(credentialEntity); + if (userEntity.getCredential(cred.getId()) != null) { + throw new ModelDuplicateException("A CredentialModel with given id already exists"); + } - return UserCredentialEntity.toModel(credentialEntity); + Map credentials = userEntity.getCredentials(); + int priority = PRIORITY_DIFFERENCE; + + if (credentials != null && !credentials.isEmpty()) { + priority = credentials.values().stream().max(MapUserCredentialEntity.ORDER_BY_PRIORITY).map(MapUserCredentialEntity::getPriority).orElse(0) + PRIORITY_DIFFERENCE; + } + + credentialEntity.setPriority(priority); + userEntity.setCredential(credentialEntity.getId(), credentialEntity); + + return MapUserCredentialEntity.toModel(credentialEntity); } @Override public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { LOG.tracef("removeStoredCredential(%s, %s, %s)%s", realm, user.getId(), id, getShortStackTrace()); - return getEntityById(realm, user.getId()) - .map(mapUserEntity -> mapUserEntity.removeCredential(id)) - .orElse(false); + + Optional entityById = getEntityById(realm, user.getId()); + if (!entityById.isPresent()) return false; + + Boolean result = entityById.get().removeCredential(id); + return result == null ? true : result; // TODO: make removeStoredCredential return Boolean so the caller can correctly handle "I don't know" null answer } @Override @@ -765,17 +807,21 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor LOG.tracef("getStoredCredentialById(%s, %s, %s)%s", realm, user.getId(), id, getShortStackTrace()); return getEntityById(realm, user.getId()) .map(mapUserEntity -> mapUserEntity.getCredential(id)) - .map(UserCredentialEntity::toModel) + .map(MapUserCredentialEntity::toModel) .orElse(null); } @Override public Stream getStoredCredentialsStream(RealmModel realm, UserModel user) { LOG.tracef("getStoredCredentialsStream(%s, %s)%s", realm, user.getId(), getShortStackTrace()); + return getEntityById(realm, user.getId()) .map(MapUserEntity::getCredentials) + .map(Map::values) + .map(Collection::stream) .orElseGet(Stream::empty) - .map(UserCredentialEntity::toModel); + .sorted(MapUserCredentialEntity.ORDER_BY_PRIORITY) + .map(MapUserCredentialEntity::toModel); } @Override @@ -795,38 +841,59 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor @Override public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) { - LOG.tracef("moveCredentialTo(%s, %s, %s, %s)%s", realm, user.getId(), id, newPreviousCredentialId, getShortStackTrace()); - String userId = user.getId(); - MapUserEntity userEntity = getEntityById(realm, userId).orElse(null); - if (userEntity == null) { - LOG.warnf("User with id: [%s] not found", userId); + + MapUserEntity userEntity = getEntityByIdOrThrow(realm, user.getId()); + + // 1 - Create new list and move everything to it. + Map credentialEntityMap = userEntity.getCredentials(); + List newList = credentialEntityMap == null ? new LinkedList<>() + : credentialEntityMap.values().stream() + .sorted(MapUserCredentialEntity.ORDER_BY_PRIORITY) + .collect(Collectors.toList()); + + // 2 - Find indexes of our and newPrevious credential + int ourCredentialIndex = -1; + int newPreviousCredentialIndex = -1; + MapUserCredentialEntity ourCredential = null; + int i = 0; + for (MapUserCredentialEntity credential : newList) { + if (id.equals(credential.getId())) { + ourCredentialIndex = i; + ourCredential = credential; + } else if(newPreviousCredentialId != null && newPreviousCredentialId.equals(credential.getId())) { + newPreviousCredentialIndex = i; + } + i++; + } + + if (ourCredentialIndex == -1) { + LOG.warnf("Not found credential with id [%s] of user [%s]", id, user.getUsername()); return false; } - // Find index of credential which should be before id in the list - int newPreviousCredentialIdIndex = -1; // If newPreviousCredentialId == null we need to put id credential to index 0 - if (newPreviousCredentialId != null) { - newPreviousCredentialIdIndex = userEntity.getCredentialIndex(newPreviousCredentialId); - if (newPreviousCredentialIdIndex == -1) { // If not null previous credential not found, print warning and return false - LOG.warnf("Credential with id: [%s] for user: [%s] not found", newPreviousCredentialId, userId); - return false; + if (newPreviousCredentialId != null && newPreviousCredentialIndex == -1) { + LOG.warnf("Can't move up credential with id [%s] of user [%s]", id, user.getUsername()); + return false; + } + + // 3 - Compute index where we move our credential + int toMoveIndex = newPreviousCredentialId==null ? 0 : newPreviousCredentialIndex + 1; + + // 4 - Insert our credential to new position, remove it from the old position + newList.add(toMoveIndex, ourCredential); + int indexToRemove = toMoveIndex < ourCredentialIndex ? ourCredentialIndex + 1 : ourCredentialIndex; + newList.remove(indexToRemove); + + // 5 - newList contains credentials in requested order now. Iterate through whole list and change priorities accordingly. + int expectedPriority = 0; + for (MapUserCredentialEntity credential : newList) { + expectedPriority += PRIORITY_DIFFERENCE; + if (credential.getPriority() != expectedPriority) { + credential.setPriority(expectedPriority); + + LOG.tracef("Priority of credential [%s] of user [%s] changed to [%d]", credential.getId(), user.getUsername(), expectedPriority); } } - - // Find current index of credential (id) which will be moved - int currentPositionOfId = userEntity.getCredentialIndex(id); - if (currentPositionOfId == -1) { - LOG.warnf("Credential with id: [%s] for user: [%s] not found", id, userId); - return false; - } - - // If id is before newPreviousCredentialId in priority list, it will be moved to position -1 - if (currentPositionOfId < newPreviousCredentialIdIndex) { - newPreviousCredentialIdIndex -= 1; - } - - // Move credential to desired index - userEntity.moveCredential(currentPositionOfId, newPreviousCredentialIdIndex + 1); return true; } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java deleted file mode 100644 index 7b2a3f0da1..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2020 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.map.user; - -import org.keycloak.common.util.Time; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientScopeModel; -import org.keycloak.models.ModelException; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserConsentModel; -import org.keycloak.models.map.common.UpdatableEntity; -import org.keycloak.models.utils.KeycloakModelUtils; - -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - - -public class UserConsentEntity extends UpdatableEntity.Impl { - - private String clientId; - private final Set grantedClientScopesIds = new HashSet<>(); - private Long createdDate; - private Long lastUpdatedDate; - - private UserConsentEntity() {} - - public static UserConsentEntity fromModel(UserConsentModel model) { - long currentTime = Time.currentTimeMillis(); - - UserConsentEntity consentEntity = new UserConsentEntity(); - consentEntity.setClientId(model.getClient().getId()); - consentEntity.setCreatedDate(currentTime); - consentEntity.setLastUpdatedDate(currentTime); - - model.getGrantedClientScopes() - .stream() - .map(ClientScopeModel::getId) - .forEach(consentEntity::addGrantedClientScopeId); - - return consentEntity; - } - - public static UserConsentModel toModel(RealmModel realm, UserConsentEntity entity) { - if (entity == null) { - return null; - } - - ClientModel client = realm.getClientById(entity.getClientId()); - if (client == null) { - throw new ModelException("Client with id " + entity.getClientId() + " is not available"); - } - UserConsentModel model = new UserConsentModel(client); - model.setCreatedDate(entity.getCreatedDate()); - model.setLastUpdatedDate(entity.getLastUpdatedDate()); - - entity.getGrantedClientScopesIds().stream() - .map(scopeId -> KeycloakModelUtils.findClientScopeById(realm, client, scopeId)) - .filter(Objects::nonNull) - .forEach(model::addGrantedClientScope); - - return model; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.updated = !Objects.equals(this.clientId, clientId); - this.clientId = clientId; - } - - public Set getGrantedClientScopesIds() { - return grantedClientScopesIds; - } - - public void addGrantedClientScopeId(String scope) { - this.updated |= grantedClientScopesIds.add(scope); - } - - public void setGrantedClientScopesIds(Set scopesIds) { - this.updated |= !Objects.equals(grantedClientScopesIds, scopesIds); - this.grantedClientScopesIds.clear(); - this.grantedClientScopesIds.addAll(scopesIds); - } - - public void removeGrantedClientScopesIds(String scopesId) { - this.updated |= this.grantedClientScopesIds.remove(scopesId); - } - - public Long getCreatedDate() { - return createdDate; - } - - public void setCreatedDate(Long createdDate) { - this.updated |= !Objects.equals(this.createdDate, createdDate); - this.createdDate = createdDate; - } - - public Long getLastUpdatedDate() { - return lastUpdatedDate; - } - - public void setLastUpdatedDate(Long lastUpdatedDate) { - this.updated |= !Objects.equals(this.lastUpdatedDate, lastUpdatedDate); - this.lastUpdatedDate = lastUpdatedDate; - } -} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java deleted file mode 100644 index c674925fa7..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.map.user; - -import org.keycloak.credential.CredentialModel; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.map.common.UpdatableEntity; - -import java.util.Objects; - -public class UserCredentialEntity extends UpdatableEntity.Impl { - - private String id; - private String type; - private String userLabel; - private Long createdDate; - private String secretData; - private String credentialData; - - UserCredentialEntity() {} - - public static UserCredentialEntity fromModel(CredentialModel model) { - UserCredentialEntity credentialEntity = new UserCredentialEntity(); - String id = model.getId() == null ? KeycloakModelUtils.generateId() : model.getId(); - credentialEntity.setId(id); - credentialEntity.setCreatedDate(model.getCreatedDate()); - credentialEntity.setUserLabel(model.getUserLabel()); - credentialEntity.setType(model.getType()); - credentialEntity.setSecretData(model.getSecretData()); - credentialEntity.setCredentialData(model.getCredentialData()); - - return credentialEntity; - } - - public static CredentialModel toModel(UserCredentialEntity entity) { - CredentialModel model = new CredentialModel(); - model.setId(entity.getId()); - model.setType(entity.getType()); - model.setCreatedDate(entity.getCreatedDate()); - model.setUserLabel(entity.getUserLabel()); - model.setSecretData(entity.getSecretData()); - model.setCredentialData(entity.getCredentialData()); - return model; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.updated |= !Objects.equals(this.id, id); - this.id = id; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.updated |= !Objects.equals(this.type, type); - this.type = type; - } - - public String getUserLabel() { - return userLabel; - } - - public void setUserLabel(String userLabel) { - this.updated |= !Objects.equals(this.userLabel, userLabel); - this.userLabel = userLabel; - } - - public Long getCreatedDate() { - return createdDate; - } - - public void setCreatedDate(Long createdDate) { - this.updated |= !Objects.equals(this.createdDate, createdDate); - this.createdDate = createdDate; - } - - public String getSecretData() { - return secretData; - } - - public void setSecretData(String secretData) { - this.updated |= !Objects.equals(this.secretData, secretData); - this.secretData = secretData; - } - - public String getCredentialData() { - return credentialData; - } - - public void setCredentialData(String credentialData) { - this.updated |= !Objects.equals(this.credentialData, credentialData); - this.credentialData = credentialData; - } -} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java deleted file mode 100644 index c73393e6b7..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.map.user; - -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.map.common.UpdatableEntity; - -import java.util.Objects; - -public class UserFederatedIdentityEntity extends UpdatableEntity.Impl { - private String token; - private String userId; - private String identityProvider; - private String userName; - - private UserFederatedIdentityEntity() {} - - public static UserFederatedIdentityEntity fromModel(FederatedIdentityModel model) { - if (model == null) return null; - UserFederatedIdentityEntity entity = new UserFederatedIdentityEntity(); - entity.setIdentityProvider(model.getIdentityProvider()); - entity.setUserId(model.getUserId()); - entity.setUserName(model.getUserName().toLowerCase()); - entity.setToken(model.getToken()); - - return entity; - } - - public static FederatedIdentityModel toModel(UserFederatedIdentityEntity entity) { - if (entity == null) return null; - return new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName(), entity.getToken()); - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.updated |= !Objects.equals(this.token, token); - this.token = token; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.updated |= !Objects.equals(this.userId, userId); - this.userId = userId; - } - - public String getIdentityProvider() { - return identityProvider; - } - - public void setIdentityProvider(String identityProvider) { - this.updated |= !Objects.equals(this.identityProvider, identityProvider); - this.identityProvider = identityProvider; - } - - public String getUserName() { - return userName; - } - - public void setUserName(String userName) { - this.updated |= !Objects.equals(this.userName, userName); - this.userName = userName; - } -} diff --git a/model/map/src/test/java/org/keycloak/models/map/user/AbstractUserEntityCredentialsOrderTest.java b/model/map/src/test/java/org/keycloak/models/map/user/AbstractUserEntityCredentialsOrderTest.java deleted file mode 100644 index dd4f85df51..0000000000 --- a/model/map/src/test/java/org/keycloak/models/map/user/AbstractUserEntityCredentialsOrderTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.map.user; - -import org.junit.Before; -import org.junit.Test; -import org.hamcrest.Matchers; -import static org.junit.Assert.assertThat; -import org.keycloak.credential.CredentialModel; - -import java.util.List; -import java.util.stream.Collectors; - -public class AbstractUserEntityCredentialsOrderTest { - - private MapUserEntity user; - - @Before - public void init() { - user = new MapUserEntity("1", "realmId"); - - for (int i = 1; i <= 5; i++) { - UserCredentialEntity credentialModel = new UserCredentialEntity(); - credentialModel.setId(Integer.toString(i)); - - user.addCredential(credentialModel); - } - } - - private void assertOrder(Integer... ids) { - List currentList = user.getCredentials().map(entity -> Integer.valueOf(entity.getId())).collect(Collectors.toList()); - assertThat(currentList, Matchers.contains(ids)); - } - - @Test - public void testCorrectOrder() { - assertOrder(1, 2, 3, 4, 5); - } - - @Test - public void testMoveToZero() { - user.moveCredential(2, 0); - assertOrder(3, 1, 2, 4, 5); - } - - @Test - public void testMoveBack() { - user.moveCredential(3, 1); - assertOrder(1, 4, 2, 3, 5); - } - - @Test - public void testMoveForward() { - user.moveCredential(1, 3); - assertOrder(1, 3, 4, 2, 5); - } - - @Test - public void testSamePosition() { - user.moveCredential(1, 1); - assertOrder(1, 2, 3, 4, 5); - } - - @Test - public void testSamePositionZero() { - user.moveCredential(0, 0); - assertOrder(1, 2, 3, 4, 5); - } - -} \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java b/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java index 0ca8497d49..ac66c03367 100644 --- a/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java +++ b/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java @@ -31,6 +31,17 @@ import java.util.stream.Stream; public interface UserCredentialStore extends Provider { void updateCredential(RealmModel realm, UserModel user, CredentialModel cred); CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred); + + /** + * Removes credential with the {@code id} for the {@code user}. + * + * @param realm realm. + * @param user user + * @param id id + * @return {@code true} if the credential was removed, {@code false} otherwise + * + * TODO: Make this method return Boolean so that store can return "I don't know" answer, this can be used for example in async stores + */ boolean removeStoredCredential(RealmModel realm, UserModel user, String id); CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id); diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index 9784b4d26f..2df2cc71bd 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -202,6 +202,8 @@ public interface UserProvider extends Provider, * @param userId id of the user * @param clientInternalId id of the client * @return {@code true} if the consent was removed, {@code false} otherwise + * + * TODO: Make this method return Boolean so that store can return "I don't know" answer, this can be used for example in async stores */ boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId); @@ -224,6 +226,8 @@ public interface UserProvider extends Provider, * @param user the user model * @param socialProvider alias of the identity provider, see {@link IdentityProviderModel#getAlias()} * @return {@code true} if the association was removed, {@code false} otherwise + * + * TODO: Make this method return Boolean so that store can return "I don't know" answer, this can be used for example in async stores */ boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider);