7 changed files with 349 additions and 6 deletions
-
2pom.xml
-
264src/main/java/br/com/luizcarlosvianamelo/keycloak/broker/oidc/mappers/ClaimToGroupMapper.java
-
40src/main/java/org/keycloak/social/discord/DiscordIdentityProvider.java
-
35src/main/java/org/keycloak/social/discord/DiscordIdentityProviderConfig.java
-
1src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
-
4src/main/resources/theme-resources/messages/admin-messages_en.properties
-
9src/main/resources/theme-resources/resources/partials/realm-identity-provider-discord-ext.html
@ -0,0 +1,264 @@ |
|||
package br.com.luizcarlosvianamelo.keycloak.broker.oidc.mappers; |
|||
|
|||
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.*; |
|||
import org.keycloak.provider.ProviderConfigProperty; |
|||
|
|||
import org.keycloak.social.discord.DiscordIdentityProviderFactory; |
|||
|
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* Class with the implementation of the identity provider mapper that sync the |
|||
* user's groups received from an external IdP into the Keycloak groups. |
|||
* |
|||
* @author Luiz Carlos Viana Melo |
|||
*/ |
|||
public class ClaimToGroupMapper extends AbstractClaimMapper { |
|||
|
|||
// logger ------------------------------------------------ |
|||
|
|||
private static final Logger logger = Logger.getLogger(ClaimToGroupMapper.class); |
|||
|
|||
// global properties ------------------------------------- |
|||
|
|||
private static final String PROVIDER_ID = "oidc-group-idp-mapper"; |
|||
|
|||
private static final String[] COMPATIBLE_PROVIDERS = { |
|||
KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, |
|||
OIDCIdentityProviderFactory.PROVIDER_ID, |
|||
DiscordIdentityProviderFactory.PROVIDER_ID |
|||
}; |
|||
|
|||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>(); |
|||
|
|||
private static final String CONTAINS_TEXT = "contains_text"; |
|||
|
|||
private static final String CREATE_GROUPS = "create_groups"; |
|||
|
|||
static { |
|||
ProviderConfigProperty property; |
|||
|
|||
property = new ProviderConfigProperty(); |
|||
property.setName(CLAIM); |
|||
property.setLabel("Claim"); |
|||
property.setHelpText("Name of claim to search for in token. This claim must be a string array with " + |
|||
"the names of the groups which the user is member. You can reference nested claims using a " + |
|||
"'.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)"); |
|||
|
|||
property.setType(ProviderConfigProperty.STRING_TYPE); |
|||
CONFIG_PROPERTIES.add(property); |
|||
|
|||
property = new ProviderConfigProperty(); |
|||
property.setName(CONTAINS_TEXT); |
|||
property.setLabel("Contains text"); |
|||
property.setHelpText("Only sync groups that contains this text in its name. If empty, sync all groups."); |
|||
|
|||
property.setType(ProviderConfigProperty.STRING_TYPE); |
|||
CONFIG_PROPERTIES.add(property); |
|||
|
|||
property = new ProviderConfigProperty(); |
|||
property.setName(CREATE_GROUPS); |
|||
property.setLabel("Create groups if not exists"); |
|||
property.setHelpText("Indicates if missing groups must be created in the realms. Otherwise, they will " + |
|||
"be ignored."); |
|||
|
|||
property.setType(ProviderConfigProperty.BOOLEAN_TYPE); |
|||
CONFIG_PROPERTIES.add(property); |
|||
} |
|||
|
|||
// properties -------------------------------------------- |
|||
|
|||
@Override |
|||
public String getId() { |
|||
return PROVIDER_ID; |
|||
} |
|||
|
|||
@Override |
|||
public String[] getCompatibleProviders() { |
|||
return COMPATIBLE_PROVIDERS; |
|||
} |
|||
|
|||
@Override |
|||
public String getDisplayCategory() { |
|||
return "Group Importer"; |
|||
} |
|||
|
|||
@Override |
|||
public String getDisplayType() { |
|||
return "Claim to Group Mapper"; |
|||
} |
|||
|
|||
@Override |
|||
public String getHelpText() { |
|||
return "If a claim exists, sync the IdP user's groups with realm groups"; |
|||
} |
|||
|
|||
@Override |
|||
public List<ProviderConfigProperty> getConfigProperties() { |
|||
return CONFIG_PROPERTIES; |
|||
} |
|||
|
|||
// actions ----------------------------------------------- |
|||
|
|||
|
|||
@Override |
|||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { |
|||
super.importNewUser(session, realm, user, mapperModel, context); |
|||
|
|||
this.syncGroups(realm, user, mapperModel, context); |
|||
} |
|||
|
|||
@Override |
|||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { |
|||
|
|||
this.syncGroups(realm, user, mapperModel, context); |
|||
} |
|||
|
|||
private void syncGroups(RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { |
|||
|
|||
// check configurations |
|||
String groupClaimName = mapperModel.getConfig().get(CLAIM); |
|||
String containsText = mapperModel.getConfig().get(CONTAINS_TEXT); |
|||
boolean createGroups = Boolean.parseBoolean(mapperModel.getConfig().get(CREATE_GROUPS)); |
|||
|
|||
// do nothing if no claim was adjusted |
|||
if (isEmpty(groupClaimName)) |
|||
return; |
|||
|
|||
// get new groups |
|||
Object newGroupsObj = getClaimValue(context, groupClaimName); |
|||
// don't modify groups membership if the claim was not found |
|||
if (newGroupsObj == null) { |
|||
logger.debugf("Realm [%s], IdP [%s]: no group claim (claim name: [%s]) for user [%s], ignoring...", |
|||
realm.getName(), |
|||
mapperModel.getIdentityProviderAlias(), |
|||
groupClaimName, |
|||
user.getUsername()); |
|||
return; |
|||
} |
|||
|
|||
logger.debugf("Realm [%s], IdP [%s]: starting mapping groups for user [%s]", |
|||
realm.getName(), |
|||
mapperModel.getIdentityProviderAlias(), |
|||
user.getUsername()); |
|||
|
|||
// convert to string list if not list |
|||
if (!List.class.isAssignableFrom(newGroupsObj.getClass())) { |
|||
List<String> newList = new ArrayList<>(); |
|||
newList.add(newGroupsObj.toString()); |
|||
newGroupsObj = newList; |
|||
} |
|||
|
|||
// get user current groups |
|||
Set<GroupModel> currentGroups = user.getGroupsStream() |
|||
.filter(g -> isEmpty(containsText) || g.getName().contains(containsText)) |
|||
.collect(Collectors.toSet()); |
|||
|
|||
logger.debugf("Realm [%s], IdP [%s]: current groups for user [%s]: %s", |
|||
realm.getName(), |
|||
mapperModel.getIdentityProviderAlias(), |
|||
user.getUsername(), |
|||
currentGroups |
|||
.stream() |
|||
.map(GroupModel::getName) |
|||
.collect(Collectors.joining(",")) |
|||
); |
|||
|
|||
// filter the groups by its name |
|||
@SuppressWarnings("unchecked") |
|||
Set<String> newGroupsNames = ((List<String>) newGroupsObj) |
|||
.stream() |
|||
.filter(t -> isEmpty(containsText) || t.contains(containsText)) |
|||
.collect(Collectors.toSet()); |
|||
|
|||
// get new groups |
|||
Set<GroupModel> newGroups = getNewGroups(realm, newGroupsNames, createGroups); |
|||
|
|||
logger.debugf("Realm [%s], IdP [%s]: new groups for user [%s]: %s", |
|||
realm.getName(), |
|||
mapperModel.getIdentityProviderAlias(), |
|||
user.getUsername(), |
|||
newGroups |
|||
.stream() |
|||
.map(GroupModel::getName) |
|||
.collect(Collectors.joining(",")) |
|||
); |
|||
|
|||
// get the groups from which the user will be removed |
|||
Set<GroupModel> removeGroups = getGroupsToBeRemoved(currentGroups, newGroups); |
|||
for (GroupModel group : removeGroups) |
|||
user.leaveGroup(group); |
|||
|
|||
// get the groups where the user will be added |
|||
Set<GroupModel> addGroups = getGroupsToBeAdded(currentGroups, newGroups); |
|||
for (GroupModel group : addGroups) |
|||
user.joinGroup(group); |
|||
|
|||
logger.debugf("Realm [%s], IdP [%s]: finishing mapping groups for user [%s]", |
|||
realm.getName(), |
|||
mapperModel.getIdentityProviderAlias(), |
|||
user.getUsername()); |
|||
} |
|||
|
|||
private Set<GroupModel> getNewGroups(RealmModel realm, Set<String> newGroupsNames, boolean createGroups) { |
|||
|
|||
Set<GroupModel> groups = new HashSet<>(); |
|||
|
|||
for (String groupName : newGroupsNames) { |
|||
GroupModel group = getGroupByName(realm, groupName); |
|||
|
|||
// create group if not found |
|||
if (group == null && createGroups) { |
|||
logger.debugf("Realm [%s]: creating group [%s]", |
|||
realm.getName(), |
|||
groupName); |
|||
|
|||
group = realm.createGroup(groupName); |
|||
} |
|||
|
|||
if (group != null) |
|||
groups.add(group); |
|||
} |
|||
|
|||
return groups; |
|||
} |
|||
|
|||
private static GroupModel getGroupByName(RealmModel realm, String name) { |
|||
|
|||
Optional<GroupModel> group = realm.getGroupsStream() |
|||
.filter(g -> g.getName().equals(name)) |
|||
.findFirst(); |
|||
|
|||
return group.orElse(null); |
|||
} |
|||
|
|||
private static Set<GroupModel> getGroupsToBeRemoved(Set<GroupModel> currentGroups, Set<GroupModel> newGroups) { |
|||
// perform a set difference |
|||
Set<GroupModel> resultSet = new HashSet<>(currentGroups); |
|||
|
|||
// (Current - New) will result in a set with the groups from which the user will be removed |
|||
resultSet.removeAll(newGroups); |
|||
|
|||
return resultSet; |
|||
} |
|||
|
|||
private static Set<GroupModel> getGroupsToBeAdded(Set<GroupModel> currentGroups, Set<GroupModel> newGroups) { |
|||
// perform a set difference |
|||
Set<GroupModel> resultSet = new HashSet<>(newGroups); |
|||
|
|||
// (New - Current) will result in a set with the groups where the user will be added |
|||
resultSet.removeAll(currentGroups); |
|||
|
|||
return resultSet; |
|||
} |
|||
|
|||
private static boolean isEmpty(String str) { |
|||
return str == null || str.length() == 0; |
|||
} |
|||
} |
@ -1 +1,2 @@ |
|||
org.keycloak.social.discord.DiscordUserAttributeMapper |
|||
br.com.luizcarlosvianamelo.keycloak.broker.oidc.mappers.ClaimToGroupMapper |
@ -1,7 +1,9 @@ |
|||
discord-client-id=Client Id |
|||
discord-client-secret=Client Secret |
|||
discord-allowed-guilds=Guild Id(s) to allow federation |
|||
discord-mapped-roles=Discord Roles mapping |
|||
discord.client-id.tooltip=Client Id for the application you created in your discord developer portal. |
|||
discord.client-secret.tooltip=Client Secret for the application that you created in your discord developer portal. |
|||
discord.allowed-guilds.tooltip=If you want to allow federation for specific guild, enter the guild id. Please use a comma as a separator for multiple guilds. |
|||
discord.default-scopes.tooltip=The scopes to be sent when asking for authorization. See discord OAuth2 documentation for possible values. If you do not specify anything, scope defaults to 'identify email' In addition, plus 'guilds' if you enter guild id(s) to allow federation. |
|||
discord.mapped-roles.tooltip=Map Discord roles to Keycloak groups. The expected format is '<guild_id>:<role_id>:<group_name>'. Use a comma as a separator for multiple mappings. |
|||
discord.default-scopes.tooltip=The scopes to be sent when asking for authorization. See discord OAuth2 documentation for possible values. If you do not specify anything, scope defaults to 'identify email' In addition, plus 'guilds' if you enter guild id(s) to allow federation. |
Write
Preview
Loading…
Cancel
Save
Reference in new issue