Introduce ProtocolMapper.getEffectiveModel to make sure values displayed in the admin console UI are 'effective' values used when processing mappers

closes #24718

Signed-off-by: mposolda <mposolda@gmail.com>

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
mposolda 2023-12-06 09:32:22 +01:00 committed by Marek Posolda
parent 16a120eafd
commit 90bf88c540
8 changed files with 176 additions and 3 deletions

View file

@ -54,4 +54,20 @@ public interface ProtocolMapper extends Provider, ProviderFactory<ProtocolMapper
default void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel client, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
};
/**
* Get effective configuration of protocol mapper. Effective configuration takes "default values" of the options into consideration
* and hence it is the configuration, which would be actually used when processing this protocolMapper during issuing tokens/assertions.
*
* So for instance, when configuration option "introspection.token.claim" is unset in the protocolMapperModel, but default value of this option is supposed to be "true", then
* effective config returned by this method will contain "introspection.token.claim" config option with value "true" . If the "introspection.token.claim" is set, then the
* default value is typically ignored in the effective configuration, but this can depend on the implementation of particular protocol mapper.
*
* @param session
* @param realm
* @param protocolMapperModel
*/
default ProtocolMapperModel getEffectiveModel(KeycloakSession session, RealmModel realm, ProtocolMapperModel protocolMapperModel) {
return protocolMapperModel;
}
}

View file

@ -22,13 +22,21 @@ import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION;
import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -155,4 +163,22 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
ClientSessionContext clientSessionCtx) {
}
@Override
public ProtocolMapperModel getEffectiveModel(KeycloakSession session, RealmModel realm, ProtocolMapperModel protocolMapperModel) {
// Effectively clone
ProtocolMapperModel copy = RepresentationToModel.toModel(ModelToRepresentation.toRepresentation(protocolMapperModel));
// UserInfo - if not set, default value is the same as includeInIDToken
if (copy.getConfig().get(INCLUDE_IN_ID_TOKEN) != null) {
copy.getConfig().put(INCLUDE_IN_USERINFO, String.valueOf(OIDCAttributeMapperHelper.includeInUserInfo(protocolMapperModel)));
}
// Introspection - if not set, default value is the same as includeInAccessToken
if (copy.getConfig().get(INCLUDE_IN_ACCESS_TOKEN) != null) {
copy.getConfig().put(INCLUDE_IN_INTROSPECTION, String.valueOf(OIDCAttributeMapperHelper.includeInIntrospection(protocolMapperModel)));
}
return copy;
}
}

View file

@ -28,7 +28,10 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
import org.keycloak.provider.ProviderConfigProperty;
@ -120,6 +123,17 @@ public class AllowedWebOriginsProtocolMapper extends AbstractOIDCProtocolMapper
return "true".equals(includeInIntrospection);
}
@Override
public ProtocolMapperModel getEffectiveModel(KeycloakSession session, RealmModel realm, ProtocolMapperModel protocolMapperModel) {
// Effectively clone
ProtocolMapperModel copy = RepresentationToModel.toModel(ModelToRepresentation.toRepresentation(protocolMapperModel));
copy.getConfig().put(INCLUDE_IN_ACCESS_TOKEN, String.valueOf(includeInAccessToken(copy)));
copy.getConfig().put(INCLUDE_IN_INTROSPECTION, String.valueOf(includeInIntrospection(copy)));
return copy;
}
private void setWebOrigin(AccessToken token, KeycloakSession session, ClientSessionContext clientSessionCtx) {
ClientModel client = clientSessionCtx.getClientSession().getClient();

View file

@ -26,7 +26,10 @@ import java.util.Map;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
@ -124,6 +127,17 @@ public class AudienceResolveProtocolMapper extends AbstractOIDCProtocolMapper im
return "true".equals(includeInIntrospection);
}
@Override
public ProtocolMapperModel getEffectiveModel(KeycloakSession session, RealmModel realm, ProtocolMapperModel protocolMapperModel) {
// Effectively clone
ProtocolMapperModel copy = RepresentationToModel.toModel(ModelToRepresentation.toRepresentation(protocolMapperModel));
copy.getConfig().put(INCLUDE_IN_ACCESS_TOKEN, String.valueOf(includeInAccessToken(copy)));
copy.getConfig().put(INCLUDE_IN_INTROSPECTION, String.valueOf(includeInIntrospection(copy)));
return copy;
}
private void setAudience(AccessToken token, ClientSessionContext clientSessionCtx, KeycloakSession session) {
String clientId = clientSessionCtx.getClientSession().getClient().getClientId();

View file

@ -113,7 +113,7 @@ public class ProtocolMappersResource {
return client.getProtocolMappersStream()
.filter(mapper -> isEnabled(session, mapper) && Objects.equals(mapper.getProtocol(), protocol))
.map(ModelToRepresentation::toRepresentation);
.map(this::toEffectiveProtocolMapperRep);
}
/**
@ -182,7 +182,7 @@ public class ProtocolMappersResource {
return client.getProtocolMappersStream()
.filter(mapper -> isEnabled(session, mapper))
.map(ModelToRepresentation::toRepresentation);
.map(this::toEffectiveProtocolMapperRep);
}
/**
@ -202,6 +202,17 @@ public class ProtocolMappersResource {
ProtocolMapperModel model = client.getProtocolMapperById(id);
if (model == null) throw new NotFoundException("Model not found");
return toEffectiveProtocolMapperRep(model);
}
private ProtocolMapperRepresentation toEffectiveProtocolMapperRep(ProtocolMapperModel model) {
ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, model.getProtocolMapper());
if (mapper == null) {
logger.warnf("Protocol mapper provider '%s' not found. Configured on mapper with ID '%s'", model.getProtocolMapper(), model.getId());
throw new NotFoundException("Protocol mapper provider not found");
}
model = mapper.getEffectiveModel(session, realm, model);
return ModelToRepresentation.toRepresentation(model);
}

View file

@ -17,6 +17,8 @@
package org.keycloak.testsuite.admin.client;
import java.util.Collections;
import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
@ -26,6 +28,9 @@ import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
@ -213,4 +218,41 @@ public class ClientProtocolMapperTest extends AbstractProtocolMapperTest {
}
}
@Test
public void test10EffectiveMappers() {
// Web origins mapper
ProtocolMapperRepresentation rep = makeMapper(OIDCLoginProtocol.LOGIN_PROTOCOL, "web-origins", AllowedWebOriginsProtocolMapper.PROVIDER_ID, Collections.emptyMap());
Response resp = oidcMappersRsc.createMapper(rep);
resp.close();
String createdId = ApiUtil.getCreatedId(resp);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientProtocolMapperPath(oidcClientId, createdId), rep, ResourceType.PROTOCOL_MAPPER);
rep = oidcMappersRsc.getMapperById(createdId);
// Test default values available on the protocol mapper
Assert.assertEquals("true", rep.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN));
Assert.assertEquals("true", rep.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION));
// Update mapper to not contain default values
rep.getConfig().remove(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
rep.getConfig().remove(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION);
oidcMappersRsc.update(createdId, rep);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientProtocolMapperPath(oidcClientId, createdId), rep, ResourceType.PROTOCOL_MAPPER);
// Test configuration will contain "effective values", which are the default values of particular options
rep = oidcMappersRsc.getMapperById(createdId);
Assert.assertEquals("true", rep.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN));
Assert.assertEquals("true", rep.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION));
// Override "includeInIntrospection"
rep.getConfig().put(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION, "false");
oidcMappersRsc.update(createdId, rep);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientProtocolMapperPath(oidcClientId, createdId), rep, ResourceType.PROTOCOL_MAPPER);
// Get mapper and check that "includeInIntrospection" is using overriden value instead of the default
rep = oidcMappersRsc.getMapperById(createdId);
Assert.assertEquals("true", rep.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN));
Assert.assertEquals("false", rep.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION));
}
}

View file

@ -17,21 +17,27 @@
package org.keycloak.testsuite.admin.client;
import java.util.Map;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.admin.client.resource.ClientScopesResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.exportimport.ExportImportUtil;
import org.keycloak.testsuite.util.AdminEventPaths;
import jakarta.ws.rs.NotFoundException;
@ -171,6 +177,50 @@ public class ClientScopeProtocolMapperTest extends AbstractProtocolMapperTest {
assertEqualMappers(rep, updated);
}
@Test
public void test08EffectiveMappers() {
ClientScopeResource rolesScope = ApiUtil.findClientScopeByName(testRealmResource(), "roles");
ProtocolMapperRepresentation audienceMapper = ExportImportUtil.findMapperByName(rolesScope.getProtocolMappers().getMappers(),
OIDCLoginProtocol.LOGIN_PROTOCOL, OIDCLoginProtocolFactory.AUDIENCE_RESOLVE);
String clientScopeID = rolesScope.toRepresentation().getId();
String protocolMapperId = audienceMapper.getId();
Map<String, String> origConfig = audienceMapper.getConfig();
try {
// Test default values available on the protocol mapper
Assert.assertEquals("true", audienceMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN));
Assert.assertEquals("true", audienceMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION));
// Update mapper to not contain default values
audienceMapper.getConfig().remove(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
audienceMapper.getConfig().remove(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION);
rolesScope.getProtocolMappers().update(protocolMapperId, audienceMapper);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientScopeProtocolMapperPath(clientScopeID, protocolMapperId), audienceMapper, ResourceType.PROTOCOL_MAPPER);
// Test configuration will contain "effective values", which are the default values of particular options
audienceMapper = rolesScope.getProtocolMappers().getMapperById(protocolMapperId);
Assert.assertEquals("true", audienceMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN));
Assert.assertEquals("true", audienceMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION));
// Override "includeInIntrospection"
audienceMapper.getConfig().put(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION, "false");
rolesScope.getProtocolMappers().update(protocolMapperId, audienceMapper);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientScopeProtocolMapperPath(clientScopeID, protocolMapperId), audienceMapper, ResourceType.PROTOCOL_MAPPER);
// Get mapper and check that "includeInIntrospection" is using overriden value instead of the default
audienceMapper = rolesScope.getProtocolMappers().getMapperById(protocolMapperId);
Assert.assertEquals("true", audienceMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN));
Assert.assertEquals("false", audienceMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION));
} finally {
audienceMapper.getConfig().putAll(origConfig);
rolesScope.getProtocolMappers().update(protocolMapperId, audienceMapper);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientScopeProtocolMapperPath(clientScopeID, protocolMapperId), audienceMapper, ResourceType.PROTOCOL_MAPPER);
}
}
@Test
public void testDeleteSamlMapper() {
ProtocolMapperRepresentation rep = makeSamlMapper("saml-role-name-mapper3");

View file

@ -461,7 +461,7 @@ public class ExportImportUtil {
Assert.assertTrue(includeInIdToken == null || Boolean.parseBoolean(includeInIdToken) == false);
}
private static ProtocolMapperRepresentation findMapperByName(List<ProtocolMapperRepresentation> mappers, String type, String name) {
public static ProtocolMapperRepresentation findMapperByName(List<ProtocolMapperRepresentation> mappers, String type, String name) {
if (mappers == null) {
return null;
}