
de.arbeitsagentur.opdt.keycloak.cassandra.user.CassandraUserAdapter Maven / Gradle / Ivy
/*
* Copyright 2022 IT-Systemhaus der Bundesagentur fuer Arbeit
*
* 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 de.arbeitsagentur.opdt.keycloak.cassandra.user;
import de.arbeitsagentur.opdt.keycloak.cassandra.AttributeTypes;
import de.arbeitsagentur.opdt.keycloak.cassandra.transaction.TransactionalModelAdapter;
import de.arbeitsagentur.opdt.keycloak.cassandra.user.persistence.UserRepository;
import de.arbeitsagentur.opdt.keycloak.cassandra.user.persistence.entities.User;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.EqualsAndHashCode;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.common.util.Time;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
@EqualsAndHashCode(callSuper = true)
@JBossLog
public abstract class CassandraUserAdapter extends TransactionalModelAdapter
implements UserModel {
public static final Boolean REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT = Boolean.FALSE;
public static final String REALM_ATTR_USERNAME_CASE_SENSITIVE =
"keycloak.username-search.case-sensitive";
public static final String NOT_BEFORE = AttributeTypes.INTERNAL_ATTRIBUTE_PREFIX + "notBefore";
public static final String REALM_ATTRIBUTE_ENABLE_CHECK_FOR_DUPLICATES_ACROSS_USERNAME_AND_EMAIL =
"enableCheckForDuplicatesAcrossUsernameAndEmail";
@EqualsAndHashCode.Exclude private final KeycloakSession session;
@EqualsAndHashCode.Exclude private final RealmModel realm;
@EqualsAndHashCode.Exclude private final UserRepository userRepository;
public CassandraUserAdapter(
KeycloakSession session, User entity, RealmModel realm, UserRepository userRepository) {
super(entity);
this.session = session;
this.realm = realm;
this.userRepository = userRepository;
}
public RealmModel getRealm() {
return realm;
}
@Override
public String getId() {
return entity.getId();
}
@Override
public String getUsername() {
return entity.getUsername();
}
public boolean hasUsername(String toCompare) {
if (toCompare == null) {
return false;
}
return isUsernameCaseSensitive(realm)
? toCompare.equals(entity.getUsername())
: KeycloakModelUtils.toLowerCaseSafe(toCompare).equals(entity.getUsernameCaseInsensitive());
}
@Override
public void setUsername(String username) {
if (username == null) {
return;
}
String usernameToCompare =
isUsernameCaseSensitive(realm) ? username : KeycloakModelUtils.toLowerCaseSafe(username);
String currentUsername =
isUsernameCaseSensitive(realm) ? entity.getUsername() : entity.getUsernameCaseInsensitive();
// Do not continue if current username of entity is the requested username
if (usernameToCompare.equals(currentUsername)) return;
if (checkUsernameUniqueness(realm, username)) {
throw new ModelDuplicateException("A user with username " + username + " already exists");
}
if (usernameEqualsExistingEmail(realm, username)) {
throw new ModelDuplicateException(
"Username cannot be set to "
+ username
+ " as this already used as email by another user");
}
User userCopy = entity.toBuilder().build();
entity.setUsername(username);
entity.setUsernameCaseInsensitive(KeycloakModelUtils.toLowerCaseSafe(username));
markUpdated(() -> userRepository.deleteUsernameSearchIndex(realm.getId(), userCopy));
}
private boolean usernameEqualsExistingEmail(RealmModel realm, String newUsername) {
if (!isCheckForDuplicatesAcrossUsernameAndEmailEnabled(realm)) {
return false;
}
if (realm.isDuplicateEmailsAllowed()) {
return false;
}
// as mail is unique (see above) the current user set username == email which is okay
if (newUsername.equals(getEmail())) {
return false;
}
return checkEmailUniqueness(realm, newUsername);
}
public static boolean isCheckForDuplicatesAcrossUsernameAndEmailEnabled(RealmModel realm) {
return realm.getAttribute(
REALM_ATTRIBUTE_ENABLE_CHECK_FOR_DUPLICATES_ACROSS_USERNAME_AND_EMAIL, false);
}
@Override
public String getEmail() {
return entity.getEmail();
}
@Override
public void setEmail(String email) {
email = KeycloakModelUtils.toLowerCaseSafe(email);
if (email != null) {
if (email.equals(getEmail())) {
return;
}
if (ObjectUtil.isBlank(email)) {
email = null;
}
}
boolean duplicatesAllowed = realm.isDuplicateEmailsAllowed();
if (!duplicatesAllowed && email != null && checkEmailUniqueness(realm, email)) {
throw new ModelDuplicateException("A user with email " + email + " already exists");
}
if (email != null && emailEqualsExistingMail(realm, email)) {
throw new ModelDuplicateException(
"Another user already uses the email " + email + " as username");
}
User userCopy = entity.toBuilder().build();
entity.setEmail(email);
markUpdated(() -> userRepository.deleteEmailSearchIndex(realm.getId(), userCopy));
}
private boolean emailEqualsExistingMail(RealmModel realm, String newEmail) {
if (!isCheckForDuplicatesAcrossUsernameAndEmailEnabled(realm)) {
return false;
}
if (realm.isDuplicateEmailsAllowed()) {
return false;
}
// as mail is unique (line above) the current user set username == email which is okay
if (getUsername().equals(newEmail)) {
return false;
}
return checkUsernameUniqueness(realm, newEmail);
}
@Override
public String getFirstName() {
return entity.getFirstName();
}
@Override
public void setFirstName(String firstName) {
entity.setFirstName(firstName);
markUpdated();
}
@Override
public String getLastName() {
return entity.getLastName();
}
@Override
public void setLastName(String lastName) {
entity.setLastName(lastName);
markUpdated();
}
@Override
public Long getCreatedTimestamp() {
// Calc timestamp as milliseconds
return entity.getCreatedTimestamp().getEpochSecond() * 1000
+ (entity.getCreatedTimestamp().getNano() / 1000000);
}
@Override
public void setCreatedTimestamp(Long timestamp) {
if (timestamp != null && timestamp <= Time.currentTimeMillis()) {
entity.setCreatedTimestamp(Instant.ofEpochMilli(timestamp));
markUpdated();
} else if (timestamp != null && timestamp > Time.currentTimeMillis()) {
log.warn("Cannot update created timestamp because it is in the future!");
}
}
@Override
public boolean isEnabled() {
return entity.getEnabled() != null && entity.getEnabled();
}
@Override
public boolean isEmailVerified() {
return entity.getEmailVerified() != null && entity.getEmailVerified();
}
@Override
public void setEmailVerified(boolean verified) {
entity.setEmailVerified(verified);
markUpdated();
}
@Override
public void setEnabled(boolean enabled) {
entity.setEnabled(enabled);
markUpdated();
}
@Override
public Map> getAttributes() {
log.debugv("get attributes: realm={0} userId={1}", realm.getId(), entity.getId());
Map> attributes = getAllAttributes();
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());
result.add(UserModel.USERNAME, entity.getUsername());
return result;
}
@Override
public void setAttribute(String name, List values) {
String valueToSet = values != null && !values.isEmpty() ? values.get(0) : null;
if (setSpecialAttributeValue(name, valueToSet)) return;
User userCopy = entity.toBuilder().build();
super.setAttribute(name, values);
addPostUpdateTask(
() -> userRepository.deleteAttributeSearchIndex(realm.getId(), userCopy, name));
}
@Override
public void setSingleAttribute(String name, String value) {
if (setSpecialAttributeValue(name, value)) return;
User userCopy = entity.toBuilder().build();
super.setAttribute(name, value);
addPostUpdateTask(
() -> userRepository.deleteAttributeSearchIndex(realm.getId(), userCopy, name));
}
@Override
public String getFirstAttribute(String name) {
return getSpecialAttributeValue(name).orElseGet(() -> getAttribute(name));
}
@Override
public void removeAttribute(String name) {
User userCopy = entity.toBuilder().build();
super.removeAttribute(name);
addPostUpdateTask(
() -> userRepository.deleteAttributeSearchIndex(realm.getId(), userCopy, name));
}
@Override
public void grantRole(RoleModel role) {
log.debugv(
"grant role mapping: realm={0} userId={1} role={2}",
realm.getId(), entity.getId(), role.getName());
if (role.isClientRole()) {
Set clientRoles =
entity.getClientRoles().getOrDefault(role.getContainerId(), new HashSet<>());
clientRoles.add(role.getId());
entity.getClientRoles().put(role.getContainerId(), clientRoles);
} else {
entity.getRealmRoles().add(role.getId());
}
markUpdated();
}
@Override
public void deleteRoleMapping(RoleModel role) {
log.debugv(
"delete role mapping: realm={0} userId={1} role={2}",
realm.getId(), entity.getId(), role.getName());
if (role.isClientRole()) {
Set clientRoles =
entity.getClientRoles().getOrDefault(role.getContainerId(), new HashSet<>());
clientRoles.remove(role.getId());
entity.getClientRoles().put(role.getContainerId(), clientRoles);
} else {
entity.getRealmRoles().remove(role.getId());
}
markUpdated();
}
@Override
public void addRequiredAction(RequiredAction action) {
entity.getRequiredActions().add(action.name());
markUpdated();
}
@Override
public void addRequiredAction(String action) {
entity.getRequiredActions().add(action);
markUpdated();
}
@Override
public void removeRequiredAction(RequiredAction action) {
entity.getRequiredActions().remove(action.name());
markUpdated();
}
@Override
public void removeRequiredAction(String action) {
entity.getRequiredActions().remove(action);
markUpdated();
}
@Override
public String getFederationLink() {
return entity.getFederationLink();
}
@Override
public void setFederationLink(String link) {
if (!Objects.equals(entity.getFederationLink(), link)) {
User userCopy = entity.toBuilder().build();
entity.setFederationLink(link);
markUpdated(() -> userRepository.deleteFederationLinkSearchIndex(realm.getId(), userCopy));
}
}
@Override
public String getServiceAccountClientLink() {
return entity.getServiceAccountClientLink();
}
@Override
public void setServiceAccountClientLink(String clientInternalId) {
if (!Objects.equals(entity.getServiceAccountClientLink(), clientInternalId)) {
entity.setServiceAccountClientLink(clientInternalId);
markUpdated();
}
}
@Override
public Stream getAttributeStream(String name) {
return getSpecialAttributeValue(name)
.map(Collections::singletonList)
.orElseGet(() -> entity.getAttribute(name))
.stream();
}
@Override
public Stream getRequiredActionsStream() {
return entity.getRequiredActions().stream();
}
@Override
public Stream getGroupsStream() {
Set groups = entity.getGroupsMembership();
if (groups == null || groups.isEmpty()) {
return Stream.empty();
}
return session.groups().getGroupsStream(realm, groups.stream());
}
@Override
public void joinGroup(GroupModel group) {
if (RoleUtils.isDirectMember(getGroupsStream(), group)) {
return;
}
entity.addGroupsMembership(group.getId());
markUpdated();
}
@Override
public void leaveGroup(GroupModel group) {
entity.removeGroupsMembership(group.getId());
markUpdated();
}
@Override
public boolean isMemberOf(GroupModel group) {
return RoleUtils.isMember(getGroupsStream(), group);
}
@Override
public Stream getRealmRoleMappingsStream() {
return getRoleMappingsStream().filter(RoleUtils::isRealmRole);
}
@Override
public Stream getClientRoleMappingsStream(ClientModel app) {
return getRoleMappingsStream().filter(r -> RoleUtils.isClientRole(r, app));
}
@Override
public boolean hasDirectRole(RoleModel role) {
return getRoleMappingsStream().map(RoleModel::getId).anyMatch(r -> r.equals(role.getId()));
}
@Override
public boolean hasRole(RoleModel role) {
return RoleUtils.hasRole(getRoleMappingsStream(), role)
|| RoleUtils.hasRoleFromGroup(getGroupsStream(), role, true);
}
@Override
public Stream getRoleMappingsStream() {
log.debugv("get role mappings: realm={0} userId={1}", realm.getId(), entity.getId());
List roleIds = new ArrayList<>();
roleIds.addAll(entity.getRealmRoles());
roleIds.addAll(
entity.getClientRoles().entrySet().stream()
.flatMap(e -> e.getValue().stream())
.collect(Collectors.toSet()));
// TODO: Remove and save Role-mappings for no longer existent roles...
return roleIds.stream().map(realm::getRoleById).filter(Objects::nonNull);
}
public abstract boolean checkEmailUniqueness(RealmModel realm, String email);
public abstract boolean checkUsernameUniqueness(RealmModel realm, String username);
@Override
public abstract SubjectCredentialManager credentialManager();
public boolean delete() {
markDeleted(); // no more updates for this user
return userRepository.deleteUser(entity.getRealmId(), entity.getId());
}
@Override
protected void flushChanges() {
userRepository.insertOrUpdate(entity);
if (entity.getServiceAccountClientLink() != null && !entity.isServiceAccount()) {
userRepository.makeUserServiceAccount(entity, realm.getId());
}
}
private Optional getSpecialAttributeValue(String name) {
if (UserModel.FIRST_NAME.equals(name)) {
return Optional.ofNullable(entity.getFirstName());
} else if (UserModel.LAST_NAME.equals(name)) {
return Optional.ofNullable(entity.getLastName());
} else if (UserModel.EMAIL.equals(name)) {
return Optional.ofNullable(entity.getEmail());
} else if (UserModel.USERNAME.equals(name)) {
return Optional.ofNullable(entity.getUsername());
}
return Optional.empty();
}
private boolean setSpecialAttributeValue(String name, String value) {
if (UserModel.FIRST_NAME.equals(name)) {
entity.setFirstName(value);
return true;
} else if (UserModel.LAST_NAME.equals(name)) {
entity.setLastName(value);
return true;
} else if (UserModel.EMAIL.equals(name)) {
setEmail(value);
return true;
} else if (UserModel.USERNAME.equals(name)) {
setUsername(value);
return true;
}
return false;
}
public static boolean isUsernameCaseSensitive(RealmModel realm) {
return realm.getAttribute(
REALM_ATTR_USERNAME_CASE_SENSITIVE, REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy