219 lines
9.0 KiB
Java
Executable File
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;
|
|
}
|
|
}
|
|
|
|
}
|