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:
parent
16a120eafd
commit
90bf88c540
8 changed files with 176 additions and 3 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue