diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java b/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java index 8ee0600b0c..6b0c2942ef 100755 --- a/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java +++ b/server-spi-private/src/main/java/org/keycloak/protocol/ProtocolMapper.java @@ -54,4 +54,20 @@ public interface ProtocolMapper extends Provider, ProviderFactoryBill Burke * @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; + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java index 5d8a7d40b4..23310d6426 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java @@ -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(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java index f7cd53051c..e4fd065922 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java @@ -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(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java index 4d82f8d067..a1346e8727 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java @@ -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); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientProtocolMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientProtocolMapperTest.java index f9e2bc80ab..fc8448f1b6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientProtocolMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientProtocolMapperTest.java @@ -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)); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeProtocolMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeProtocolMapperTest.java index 9b84cd7f6b..f723a0a414 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeProtocolMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeProtocolMapperTest.java @@ -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 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"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index 9b9fd05e02..a11e48c592 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -461,7 +461,7 @@ public class ExportImportUtil { Assert.assertTrue(includeInIdToken == null || Boolean.parseBoolean(includeInIdToken) == false); } - private static ProtocolMapperRepresentation findMapperByName(List mappers, String type, String name) { + public static ProtocolMapperRepresentation findMapperByName(List mappers, String type, String name) { if (mappers == null) { return null; }