8000 Add Array support to 'User Session Note' mappers. by artur-baltabayev · Pull Request #45939 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,34 +105,84 @@ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserMo
}

private void addClaimsToSessionNote(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
Map<String, List<String>> claims = mapperModel.getConfigMap(CLAIMS_PROPERTY_NAME);
boolean areClaimValuesRegex =
final var claims = mapperModel.getConfigMap(CLAIMS_PROPERTY_NAME);
final var areClaimValuesRegex =
Boolean.parseBoolean(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME));

for (Map.Entry<String, List<String>> claim : claims.entrySet()) {
Object claimValueObj = getClaimValue(context, claim.getKey());
final var claimValueObj = getClaimValue(context, claim.getKey());
if (claimValueObj == null) {
continue;
}

final var brokeredUserId = context.getBrokerUserId();
final var claimKey = claim.getKey();

for (String value : claim.getValue()) {
if (claimValueObj != null) {
if (!(claimValueObj instanceof String)) {
LOG.warnf(
"Claim '%s' does not contain a string value for user with brokerUserId '%s'. "
+ "Actual value is of type '%s': %s",
claim.getKey(),
context.getBrokerUserId(), claimValueObj.getClass(), claimValueObj);
continue;
}

String claimValue = (String) claimValueObj;

boolean claimValuesMatch = areClaimValuesRegex ? valueMatchesRegex(value, claimValue)
: valueEquals(value, claimValue);

if (claimValuesMatch) {
context.setSessionNote(claim.getKey(), claimValue);
}
final var claimValue = getClaimValueString(brokeredUserId, claimKey, claimValueObj);
if (claimValue == null) {
continue;
}
Comment on lines 121 to +125

final var claimValuesMatch = areClaimValuesRegex ? valueMatchesRegex(value, claimValue)
: valueEquals(value, claimValue);

if (claimValuesMatch) {
context.setSessionNote(claim.getKey(), claimValue);
}
}
}
}

private String getClaimValueString(final String brokeredUserId, final String claimKey, final Object claimValueObj) {
var result = "";

if (claimValueObj instanceof List) {
result = convertArrayClaimToString(brokeredUserId, claimKey, claimValueObj);
} else if (claimValueObj instanceof String claimValueString) {
result = claimValueString;
} else {
LOG.warnf("Claim '%s':%s contains a value with an unsupported type (%s) for user with brokerUserId '%s'. "
+ "The supported types are either String or List.", claimKey, claimValueObj,
claimValueObj.getClass(), brokeredUserId);

return null;
}

return result;
}

private String convertArrayClaimToString(final String brokeredUserId, final String claimKey,
final Object claimValueObj) {
var result = String.valueOf(claimValueObj);
result = result.replace(" ", "");
result = result.replace("[", "");
result = result.replace("]", "");

final var resultIsInvalid = containsMalformedEntries(brokeredUserId, claimKey, claimValueObj, result);
if (Boolean.TRUE.equals(resultIsInvalid)) {
return null;
}

return result;
}
Comment on lines +155 to +168

private Boolean containsMalformedEntries(final String brokeredUserId, final String claimKey,
final Object claimValueObj, final String stringArray) {
var foundEntryWithIllegalCharacter = false;
final var entries = stringArray.split(",");

for(String entry: entries) {
final var entryIsValid = entry.matches("^[a-zA-Z0-9._-]+$");
if (!entryIsValid) {
LOG.warnf("Claim \"%s\": %s contains a String \"%s\" with unsupported characters for user with brokeredUserId '%s'",
claimKey, claimValueObj, entry, brokeredUserId);

foundEntryWithIllegalCharacter = true;
break;
}
}

return foundEntryWithIllegalCharacter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ private static Object convertToType(String type, Object attributeValue) {
return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getInteger);
}
return null;
case "Array":
if (attributeValue instanceof List) return attributeValue;
if (attributeValue instanceof String attributeValueString) {
final var entries = attributeValueString.split(",");
return List.of(entries);
}
return List.of(attributeValue);
Comment on lines +225 to +231
case "JSON":
JsonNode jsonNodeObject = getJsonNode(attributeValue);
if (jsonNodeObject != null) return jsonNodeObject;
Expand Down Expand Up @@ -469,7 +476,7 @@ public static void addTokenClaimNameConfig(List<ProviderConfigProperty> configPr
}

public static void addJsonTypeConfig(List<ProviderConfigProperty> configProperties) {
addJsonTypeConfig(configProperties, List.of("String", "long", "int", "boolean", "JSON"), null);
addJsonTypeConfig(configProperties, List.of("Array", "String", "long", "int", "boolean", "JSON"), null);
}

public static void addJsonTypeConfig(List<ProviderConfigProperty> configProperties, List<String> supportedTypes, String defaultValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.keycloak.admin.client.CreatedResponseUtil;
Expand Down Expand Up @@ -37,13 +38,16 @@ public class OidcClaimToUserSessionNoteMapperTest extends AbstractIdentityProvid

private static final String CLAIM_NAME = "sessionNoteTest";
private static final String CLAIM_VALUE = "foo";

private static final String ARRAY_CLAIM_JSON_TYPE = "Array";
private static final String CONFIG_PROPERTY_CLAIMS = "claims";

private static final String HARD_CODED_CLAIM_CONFIG_PROPERTY_CLAIM_VALUE = "claim.value";

private ClientRepresentation consumerClientRep;
private String providerClientUuid;
private String providerHardcodedClaimMapperId;
private String consumerSessionNoteToClaimMapperId;

@Override
protected BrokerConfiguration getBrokerConfiguration() {
Expand All @@ -68,8 +72,9 @@ public void setup() {
consumerSessionNoteToClaimMapper.setProtocolMapper(UserSessionNoteMapper.PROVIDER_ID);
consumerSessionNoteToClaimMapper.setConfig(Map.of("user.session.note", CLAIM_NAME, "claim.name", CLAIM_NAME,
"access.token.claim", "true"));
CreatedResponseUtil.getCreatedId(consumerRealm.clients().get(consumerClientRep.getId()).getProtocolMappers()
.createMapper(consumerSessionNoteToClaimMapper));
consumerSessionNoteToClaimMapperId = CreatedResponseUtil
.getCreatedId(consumerRealm.clients().get(consumerClientRep.getId()).getProtocolMappers()
.createMapper(consumerSessionNoteToClaimMapper));

RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
ClientRepresentation providerClientRep = providerRealm.clients().findByClientId("brokerapp").get(0);
Expand Down Expand Up @@ -122,6 +127,19 @@ public void claimIsPropagatedOnAllLoginsWhenNameMatchesAndSyncModeIsForce() {
assertThat(accessTokenSecondLogin.getOtherClaims().get(CLAIM_NAME), equalTo(updatedClaimValue));
}

@Test
public void claimWithArrayValueIsMappedAsExpected() {
updateSessionNoteToClaimMapper(ARRAY_CLAIM_JSON_TYPE);
createUserSessionNoteIdpMapper(IdentityProviderMapperSyncMode.FORCE, "foo-1,foo-2");

final var updatedClaimValue = "foo-1,foo-2";
updateProviderHardcodedClaimMapper(updatedClaimValue);
Comment on lines +131 to +136

final var accessTokenFirstLogin = login();
final var expectedClaimValue = List.of("foo-1", "foo-2");
assertThat(accessTokenFirstLogin.getOtherClaims().get(CLAIM_NAME), equalTo(expectedClaimValue));
}

@Test
public void claimIsNotPropagatedWhenNameDoesNotMatch() {
createUserSessionNoteIdpMapper(IdentityProviderMapperSyncMode.IMPORT, "something-unexpected-1", "something-unexpected-2");
Expand Down Expand Up @@ -198,7 +216,6 @@ private void updateProviderHardcodedClaimMapper(String value) {
clientProtocolMappersResource.update(mapper.getId(), mapper);
}


private void updateUserSessionNoteIdpMapper(IdentityProviderMapperRepresentation mapper, String matchingValue) {
Map<String, String> existingConfig = mapper.getConfig();
Map<String, String> newConfig = existingConfig == null ? new HashMap<>() : existingConfig;
Expand All @@ -209,6 +226,18 @@ private void updateUserSessionNoteIdpMapper(IdentityProviderMapperRepresentation
idpResource.update(mapper.getId(), mapper);
}

private void updateSessionNoteToClaimMapper(String claimJsonType) {
final var consumerRealm = adminClient.realm(bc.consumerRealmName());
final var clientProtocolMappersResource = consumerRealm.clients().get(consumerClientRep.getId()).getProtocolMappers();
final var mapper = clientProtocolMappersResource.getMapperById(consumerSessionNoteToClaimMapperId);

final var existingConfig = mapper.getConfig();
Map<String, String> newConfig = existingConfig == null ? new HashMap<>() : existingConfig;
newConfig.put("jsonType.label", claimJsonType);

clientProtocolMappersResource.update(mapper.getId(), mapper);
Comment on lines +234 to +238
}

private IdentityProviderMapperRepresentation persistMapper(IdentityProviderMapperRepresentation idpMapper) {
String idpAlias = bc.getIDPAlias();
IdentityProviderResource idpResource = realm.identityProviders().get(idpAlias);
Expand Down
Loading
0