keycloak-d4science-spi-parent/identity-provider-mapper/src/main/java/org/gcube/keycloak/broker/oidc/mappers/UsernameFromMailMapper.java

219 lines
9.0 KiB
Java
Executable File

package org.gcube.keycloak.broker.oidc.mappers;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.mappers.AbstractClaimMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory;
import org.keycloak.social.facebook.FacebookIdentityProviderFactory;
import org.keycloak.social.github.GitHubIdentityProviderFactory;
import org.keycloak.social.gitlab.GitLabIdentityProviderFactory;
import org.keycloak.social.google.GoogleIdentityProviderFactory;
import org.keycloak.social.instagram.InstagramIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
import org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory;
import org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory;
import org.keycloak.social.openshift.OpenshiftV4IdentityProviderFactory;
import org.keycloak.social.paypal.PayPalIdentityProviderFactory;
import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory;
import org.keycloak.social.twitter.TwitterIdentityProviderFactory;
/**
* @author <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
*/
public class UsernameFromMailMapper extends AbstractClaimMapper {
private static final Logger logger = Logger.getLogger(UsernameFromMailMapper.class);
private static final Character PERIOD = '.';
private static final Character DASH = '-';
private static final String CYRUS = "cyrus";
private static final String POSTFIX = "postfix";
private static final String COMMA = ",";
public static final String[] COMPATIBLE_PROVIDERS = {
OIDCIdentityProviderFactory.PROVIDER_ID,
BitbucketIdentityProviderFactory.PROVIDER_ID,
FacebookIdentityProviderFactory.PROVIDER_ID,
KeycloakOIDCIdentityProviderFactory.PROVIDER_ID,
GitHubIdentityProviderFactory.PROVIDER_ID,
GitLabIdentityProviderFactory.PROVIDER_ID,
GoogleIdentityProviderFactory.PROVIDER_ID,
InstagramIdentityProviderFactory.PROVIDER_ID,
LinkedInIdentityProviderFactory.PROVIDER_ID,
MicrosoftIdentityProviderFactory.PROVIDER_ID,
OpenshiftV3IdentityProviderFactory.PROVIDER_ID,
OpenshiftV4IdentityProviderFactory.PROVIDER_ID,
PayPalIdentityProviderFactory.PROVIDER_ID,
StackoverflowIdentityProviderFactory.PROVIDER_ID,
TwitterIdentityProviderFactory.PROVIDER_ID
};
private static final List<ProviderConfigProperty> configProperties;
private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(
Arrays.asList(IdentityProviderSyncMode.values()));
public static final String RESERVED_USERNAMES = "reserved-usernames";
public static final String AUTO_RESOLVE_CONFLICT = "auto-resolve";
static {
configProperties = ProviderConfigurationBuilder.create().property().name(RESERVED_USERNAMES)
.label("Reserved Usernames")
.helpText("List of reserved usernames (comma separated) that cannot be accepted. "
+ "If found a progressive suffix number will we added.")
.type(ProviderConfigProperty.STRING_TYPE).defaultValue(CYRUS + COMMA + POSTFIX)
.add().property().name(AUTO_RESOLVE_CONFLICT).label("Auto resolve conflicts")
.helpText("Automatically add a numeric suffix to avoid already existing username conflict.")
.type(ProviderConfigProperty.BOOLEAN_TYPE).add().build();
}
public static final String PROVIDER_ID = "username-from-idp-email-mapper";
@Override
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String[] getCompatibleProviders() {
return COMPATIBLE_PROVIDERS;
}
@Override
public String getDisplayCategory() {
return "Preprocessor";
}
@Override
public String getDisplayType() {
return "Username from IdP email importer";
}
@Override
public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
// preprocessFederatedIdentity gets called anyways, so we only need to set the username if necessary.
// However, we don't want to set the username when the email is used as username
if (!realm.isRegistrationEmailAsUsername()) {
user.setUsername(context.getModelUsername());
}
}
@Override
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm,
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
Set<String> reservedUsernames = getConfigValuesOrEmptySetIfNullOrEmptyString(
mapperModel.getConfig().get(RESERVED_USERNAMES));
logger.debugf("Reserved usernames are: %s", reservedUsernames);
boolean autoResolveConflicts = Boolean.valueOf(mapperModel.getConfig().get(AUTO_RESOLVE_CONFLICT));
logger.debugf("Auto resolve conflict setting is: %b", autoResolveConflicts);
String email = context.getEmail();
logger.debugf("Email address is: %s", email);
String username = email.substring(0, email.indexOf('@')).toLowerCase();
logger.debugf("Extracted raw username is: %s", username);
for (Character c : username.chars().mapToObj(e -> (char) e).collect(Collectors.toSet())) {
if (!isChar(c) && !isDigit(c) && (c != DASH) && (c != PERIOD)) {
logger.infof("Replacing unneded char (%c) with %c", c, PERIOD);
username = username.replace(c, PERIOD);
}
}
boolean usernameAlreadyExists = usernameAlreadyExists(session, realm, username);
if ((usernameAlreadyExists && autoResolveConflicts)
|| reservedUsernames.contains(username)) {
if (usernameAlreadyExists) {
logger.infof("Username already exists: %s", username);
} else {
logger.info("Username is one of the reserved usernames");
}
for (int i = 1;; i++) {
String tempUsername = username + PERIOD + i;
logger.tracef("Trying with username: %s", tempUsername);
if (usernameAlreadyExists(session, realm, tempUsername)) {
logger.tracef("Username already exists: %s", tempUsername);
} else {
logger.tracef("Username is OK: %s", tempUsername);
username = tempUsername;
break;
}
}
}
logger.infof("Final computed username is: %s", username);
context.setModelUsername(username);
}
private static boolean isChar(char c) {
return (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')));
}
private static boolean isDigit(char c) {
return ((c >= '0') && (c <= '9'));
}
private boolean usernameAlreadyExists(KeycloakSession session, RealmModel realm, String username) {
return KeycloakModelUtils.findUserByNameOrEmail(session, realm, username) != null;
}
@Override
public String getHelpText() {
return "Extract the IdP username from the e-mail address (before the '@' char).";
}
protected Set<String> getConfigValuesOrEmptySetIfNullOrEmptyString(String str) {
if (str == null || "".equals(str)) {
return Collections.emptySet();
} else {
String[] objClasses = str.split(COMMA);
Set<String> trimmed = new HashSet<>();
for (String objectClass : objClasses) {
objectClass = objectClass.trim();
if (objectClass.length() > 0) {
trimmed.add(objectClass);
}
}
return trimmed;
}
}
}