diff --git a/CHANGELOG.md b/CHANGELOG.md index 45cf2a6..cb50b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm # Changelog for "keycloak-d4science-spi-parent" ## [v2.1.0-SNAPSHOT] - +- Added new [protocol-mapper](protocol-mapper/README.md) module to make the custom protocol mappers available +- Revised terms, EU links, D4Science and Blue-Cloud logo updated (#25444) ## [v2.0.0] - Updated to be compiled/used with Keycloak v19.0.2 diff --git a/README.md b/README.md index 513d2b8..c0fb4ef 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The project is a Maven master POM project and it is composed of several modules: * [keycloak-d4science-theme](keycloak-d4science-theme/README.md) * [keycloak-d4science-script](keycloak-d4science-script/README.md) * [ldap-storage-mapper](ldap-storage-mapper/README.md) +* [protocol-mapper](protocol-mapper/README.md) ## Built With diff --git a/pom.xml b/pom.xml index adb00bc..a3dbe7a 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ org.keycloak keycloak-parent 19.0.2 + pom import @@ -95,6 +96,7 @@ org.slf4j slf4j-log4j12 test + log4j diff --git a/protocol-mapper/CHANGELOG.md b/protocol-mapper/CHANGELOG.md index 0ff63e6..b03ce1b 100644 --- a/protocol-mapper/CHANGELOG.md +++ b/protocol-mapper/CHANGELOG.md @@ -3,4 +3,4 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm # Changelog for "identity-provider-mapper" ## [v2.1.0-SNAPSHOT] -- Added new module to make the custom protocol mappers available +- Provided the `D4ScienceContextMapper` that maps the D4S context requested in a customizable HTTP header as token's claim having the configured name (defaulting to the 'aud' claim). Can also shrink the `resource access` token claim to have only the requested context entry. diff --git a/protocol-mapper/pom.xml b/protocol-mapper/pom.xml index 57a72c1..732e1e0 100644 --- a/protocol-mapper/pom.xml +++ b/protocol-mapper/pom.xml @@ -1,4 +1,6 @@ - + 4.0.0 @@ -12,15 +14,19 @@ jar - scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git - scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git - https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId} + + scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git + + scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git + + https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId} 5.8.2 3.22.0 4.5.1 + 2.1.1 @@ -41,6 +47,12 @@ ${org-mockito.version} test + + + + + + diff --git a/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java b/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java index 74f6175..e0aa3c3 100644 --- a/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java +++ b/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java @@ -13,27 +13,51 @@ import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessToken.Access; import org.keycloak.representations.IDToken; public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { private static final Logger logger = Logger.getLogger(D4ScienceContextMapper.class); - private static final List configProperties = new ArrayList<>(); + public static final String HTTP_REQUEST_HEADER_NAME = "d4scm.header-name"; + public static final String NARROW_RESOURCE_ACCESS = "d4scm.narrow-ra"; + + private static final List CONFIG_PROPERTIES = new ArrayList<>(); // Assuring that the mapper is executed as last private static final int PRIORITY = Integer.MAX_VALUE; private static final String DISPLAY_TYPE = "OIDC D4Science Context Mapper"; private static final String PROVIDER_ID = "oidc-d4scince-context-mapper"; - public static final String HEADER_NAME = "X-D4Science-Context"; -// public static final String HEADER_NAME = "X-Infrastructure-Context"; -// public static final String HEADER_NAME = "X-Infra-Context"; - + public static final String DEFAULT_HEADER_NAME = "X-D4Science-Context"; + public static final String DEFAULT_TOKEN_CLAIM = "aud"; static { - OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties); - OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, D4ScienceContextMapper.class); + OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG_PROPERTIES); + CONFIG_PROPERTIES.forEach(conf -> { + if (OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME.equals(conf.getName())) + conf.setDefaultValue(DEFAULT_TOKEN_CLAIM); + conf.setReadOnly(true); + }); + + OIDCAttributeMapperHelper.addIncludeInTokensConfig(CONFIG_PROPERTIES, D4ScienceContextMapper.class); + + ProviderConfigProperty headerProperty = new ProviderConfigProperty(); + headerProperty.setName(HTTP_REQUEST_HEADER_NAME); + headerProperty.setLabel("HTTP request header name with the requested context"); + headerProperty.setType(ProviderConfigProperty.STRING_TYPE); + headerProperty.setHelpText("The HTTP header that contains the requested context to be mapped as the requested in the configured claim"); + headerProperty.setDefaultValue(DEFAULT_HEADER_NAME); + headerProperty.setReadOnly(true); + CONFIG_PROPERTIES.add(headerProperty); + + ProviderConfigProperty narrowProperty = new ProviderConfigProperty(); + narrowProperty.setName(NARROW_RESOURCE_ACCESS); + narrowProperty.setLabel("Narrow down resource access array?"); + narrowProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE); + narrowProperty.setHelpText("Narrow down resource access claim to contain only the requested context entry"); + CONFIG_PROPERTIES.add(narrowProperty); } @Override @@ -53,12 +77,12 @@ public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implement @Override public String getHelpText() { - return "Maps the D4Science context audience by reading the '" + HEADER_NAME + "' header and sets it as the configured token claim"; + return "Maps the D4Science context audience by reading the configured header's value and sets it as the configured token claim, if it is in scope"; } @Override public List getConfigProperties() { - return configProperties; + return CONFIG_PROPERTIES; } @Override @@ -76,17 +100,28 @@ public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implement // Since only the OIDCAccessTokenMapper interface is implemented, we are almost sure that // the token object is an AccessToken but adding a specific check anyway if (token instanceof AccessToken) { - logger.debugf("Looking for the '%s' header", HEADER_NAME); - String requestedD4SContext = keycloakSession.getContext().getRequestHeaders().getHeaderString(HEADER_NAME); + AccessToken accessToken = ((AccessToken) token); + String headerName = mappingModel.getConfig().get(HTTP_REQUEST_HEADER_NAME); + if (headerName == null || "".equals(headerName)) { + headerName = DEFAULT_HEADER_NAME; + } + logger.debugf("Looking for the '%s' header", headerName); + String requestedD4SContext = keycloakSession.getContext().getRequestHeaders().getHeaderString(headerName); if (requestedD4SContext != null && !"".equals(requestedD4SContext)) { logger.debugf("Checking resource access for the requested context: %s", requestedD4SContext); - - if (((AccessToken) token).getResourceAccess().containsKey(requestedD4SContext)) { + Access contextAccessInResourceAccess = accessToken.getResourceAccess(requestedD4SContext); + if (contextAccessInResourceAccess != null) { logger.debugf("Mapping it as the configured claim: %s", mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME)); OIDCAttributeMapperHelper.mapClaim(token, mappingModel, requestedD4SContext); + + if (Boolean.parseBoolean(mappingModel.getConfig().get(NARROW_RESOURCE_ACCESS))) { + // Removing all access details but the requested context + accessToken.getResourceAccess().clear(); + accessToken.getResourceAccess().put(requestedD4SContext, contextAccessInResourceAccess); + } } else { logger.warnf("Requested context '%s' is not accessible to the client: %s", requestedD4SContext, clientSessionCtx.getClientSession().getClient().getName()); diff --git a/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java b/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java index 1829ad3..0968079 100644 --- a/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java +++ b/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java @@ -6,11 +6,11 @@ import static org.mockito.Mockito.when; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import javax.ws.rs.core.HttpHeaders; -import org.assertj.core.util.Maps; import org.junit.Test; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; @@ -31,7 +31,6 @@ import org.mockito.Mockito; */ public class D4ScienceContextMapperTest { - static final String CLAIM_NAME = "haandlerIdClaimNameExample"; static final String HEADER_VALUE = "ginostilla"; @Test @@ -62,31 +61,44 @@ public class D4ScienceContextMapperTest { .collect(Collectors.toList()); assertThat(configPropertyNames).containsExactly(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, - OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN); + OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, D4ScienceContextMapper.HTTP_REQUEST_HEADER_NAME, + D4ScienceContextMapper.NARROW_RESOURCE_ACCESS); } @Test - public void shouldAddClaim() { + public void shouldAddClaimAndNotNarrow() { final UserSessionModel session = givenUserSession(); final KeycloakSession keycloakSession = givenKeycloakSession(true); - final AccessToken accessToken = transformAccessToken(session, keycloakSession, true); - assertThat(accessToken.getOtherClaims().get(CLAIM_NAME)).isEqualTo(HEADER_VALUE); + final AccessToken accessToken = transformAccessToken(session, keycloakSession, true, false); + assertThat(accessToken.getAudience()[0]).isEqualTo(HEADER_VALUE); + assertThat(accessToken.getResourceAccess().size()).isEqualTo(2); + assertThat(accessToken.getResourceAccess().keySet()).contains(HEADER_VALUE); + } + + @Test + public void shouldAddClaimAndNarrow() { + final UserSessionModel session = givenUserSession(); + final KeycloakSession keycloakSession = givenKeycloakSession(true); + final AccessToken accessToken = transformAccessToken(session, keycloakSession, true, true); + assertThat(accessToken.getAudience()[0]).isEqualTo(HEADER_VALUE); + assertThat(accessToken.getResourceAccess().size()).isEqualTo(1); + assertThat(accessToken.getResourceAccess().keySet().iterator().next()).isEqualTo(HEADER_VALUE); } @Test public void shouldNotAddClaim() { final UserSessionModel session = givenUserSession(); final KeycloakSession keycloakSession = givenKeycloakSession(false); - final AccessToken accessToken = transformAccessToken(session, keycloakSession, true); - assertThat(accessToken.getOtherClaims().get(CLAIM_NAME)).isNull(); + final AccessToken accessToken = transformAccessToken(session, keycloakSession, true, false); + assertThat(accessToken.getAudience()).isNull(); } @Test public void shouldNotAddClaimAndLogWarning() { final UserSessionModel session = givenUserSession(); final KeycloakSession keycloakSession = givenKeycloakSession(true); - final AccessToken accessToken = transformAccessToken(session, keycloakSession, false); - assertThat(accessToken.getOtherClaims().get(CLAIM_NAME)).isNull(); + final AccessToken accessToken = transformAccessToken(session, keycloakSession, false, false); + assertThat(accessToken.getAudience()).isNull(); } private UserSessionModel givenUserSession() { @@ -104,21 +116,22 @@ public class D4ScienceContextMapperTest { when(context.getRequestHeaders()).thenReturn(headers); if (withHeader) { - when(headers.getHeaderString(D4ScienceContextMapper.HEADER_NAME)).thenReturn(HEADER_VALUE); + when(headers.getHeaderString(D4ScienceContextMapper.DEFAULT_HEADER_NAME)).thenReturn(HEADER_VALUE); } else { - when(headers.getHeaderString(D4ScienceContextMapper.HEADER_NAME)).thenReturn(""); + when(headers.getHeaderString(D4ScienceContextMapper.DEFAULT_HEADER_NAME)).thenReturn(""); } return keycloakSession; } private AccessToken transformAccessToken(UserSessionModel userSessionModel, KeycloakSession keycloakSession, - boolean withResourceAccess) { + boolean withResourceAccess, boolean withNarrowRA) { final ProtocolMapperModel mappingModel = new ProtocolMapperModel(); - mappingModel.setConfig(createConfig()); + mappingModel.setConfig(createConfig(withNarrowRA)); AccessToken at = new AccessToken(); if (withResourceAccess) { - at.setResourceAccess(Maps.newHashMap(HEADER_VALUE, null)); + at.addAccess(HEADER_VALUE); + at.addAccess(UUID.randomUUID().toString()); } return new D4ScienceContextMapper().transformAccessToken(at, mappingModel, keycloakSession, @@ -135,11 +148,13 @@ public class D4ScienceContextMapperTest { return csc; } - private Map createConfig() { - final Map result = new HashMap<>(); - result.put("access.token.claim", "true"); - result.put("claim.name", CLAIM_NAME); - return result; + private Map createConfig(boolean narrowRA) { + final Map config = new HashMap<>(); + config.put(D4ScienceContextMapper.HTTP_REQUEST_HEADER_NAME, D4ScienceContextMapper.DEFAULT_HEADER_NAME); + config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, D4ScienceContextMapper.DEFAULT_TOKEN_CLAIM); + config.put(D4ScienceContextMapper.NARROW_RESOURCE_ACCESS, Boolean.toString(narrowRA)); + config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + return config; } } \ No newline at end of file