Support to define compatible mappers for (new) Identity Providers

- Also allows to use existing mappers for custom Identity Providers without having to change those mappers

Closes #21154
This commit is contained in:
Daniel Fesenmeyer 2023-08-22 18:44:03 +02:00 committed by Pedro Igor
parent 442adfa495
commit a68ad55a37
7 changed files with 138 additions and 24 deletions

View file

@ -166,4 +166,5 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
public IdentityProviderDataMarshaller getMarshaller() {
return new DefaultDataMarshaller();
}
}

View file

@ -29,6 +29,9 @@ import org.keycloak.sessions.AuthenticationSessionModel;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.util.Arrays;
import java.util.List;
/**
* @author Pedro Igor
*/
@ -74,6 +77,8 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
Response error(String message);
}
C getConfig();
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context);
@ -132,4 +137,13 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/
IdentityProviderDataMarshaller getMarshaller();
/**
* Checks whether a mapper is supported for this Identity Provider.
*/
default boolean isMapperSupported(IdentityProviderMapper mapper) {
List<String> compatibleIdps = Arrays.asList(mapper.getCompatibleProviders());
return compatibleIdps.contains(IdentityProviderMapper.ANY_PROVIDER)
|| compatibleIdps.contains(getConfig().getProviderId());
}
}

View file

@ -64,9 +64,7 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@ -243,13 +241,14 @@ public class IdentityProviderResource {
}
private IdentityProviderFactory getIdentityProviderFactory() {
private IdentityProviderFactory<?> getIdentityProviderFactory() {
String providerId = identityProviderModel.getProviderId();
return Stream.concat(session.getKeycloakSessionFactory().getProviderFactoriesStream(IdentityProvider.class),
session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class))
.filter(providerFactory -> Objects.equals(providerFactory.getId(), identityProviderModel.getProviderId()))
.filter(providerFactory -> Objects.equals(providerFactory.getId(), providerId))
.map(IdentityProviderFactory.class::cast)
.findFirst()
.orElse(null);
.orElseThrow(() -> new IllegalStateException("IDP not found by Provider ID: " + providerId));
}
/**
@ -271,13 +270,17 @@ public class IdentityProviderResource {
}
try {
IdentityProviderFactory factory = getIdentityProviderFactory();
return factory.create(session, identityProviderModel).export(session.getContext().getUri(), realm, format);
return createIdentityProviderInstance().export(session.getContext().getUri(), realm, format);
} catch (Exception e) {
throw ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND);
}
}
private IdentityProvider<?> createIdentityProviderInstance() {
IdentityProviderFactory<?> factory = getIdentityProviderFactory();
return factory.create(session, identityProviderModel);
}
/**
* Get mapper types for identity provider
*/
@ -293,26 +296,22 @@ public class IdentityProviderResource {
throw new jakarta.ws.rs.NotFoundException();
}
IdentityProvider<?> identityProviderInstance = createIdentityProviderInstance();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
return sessionFactory.getProviderFactoriesStream(IdentityProviderMapper.class)
.map(IdentityProviderMapper.class::cast)
.map(mapper -> Arrays.stream(mapper.getCompatibleProviders())
.filter(type -> Objects.equals(IdentityProviderMapper.ANY_PROVIDER, type) ||
Objects.equals(identityProviderModel.getProviderId(), type))
.map(type -> {
IdentityProviderMapperTypeRepresentation rep = new IdentityProviderMapperTypeRepresentation();
rep.setId(mapper.getId());
rep.setCategory(mapper.getDisplayCategory());
rep.setName(mapper.getDisplayType());
rep.setHelpText(mapper.getHelpText());
rep.setProperties(mapper.getConfigProperties().stream()
.map(ModelToRepresentation::toRepresentation)
.collect(Collectors.toList()));
return rep;
})
.findFirst()
.orElse(null))
.filter(Objects::nonNull)
.filter(identityProviderInstance::isMapperSupported)
.map(mapper -> {
IdentityProviderMapperTypeRepresentation rep = new IdentityProviderMapperTypeRepresentation();
rep.setId(mapper.getId());
rep.setCategory(mapper.getDisplayCategory());
rep.setName(mapper.getDisplayType());
rep.setHelpText(mapper.getHelpText());
rep.setProperties(mapper.getConfigProperties().stream()
.map(ModelToRepresentation::toRepresentation)
.collect(Collectors.toList()));
return rep;
})
.collect(Collectors.toMap(IdentityProviderMapperTypeRepresentation::getId, Function.identity()));
}

View file

@ -0,0 +1,30 @@
package org.keycloak.testsuite.broker.oidc;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.models.KeycloakSession;
import java.util.Arrays;
import java.util.List;
/**
* @author Daniel Fesenmeyer <daniel.fesenmeyer@bosch.com>
*/
public class OverwrittenMappersTestIdentityProvider extends KeycloakOIDCIdentityProvider {
public OverwrittenMappersTestIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
super(session, config);
}
@Override
public boolean isMapperSupported(IdentityProviderMapper mapper) {
List<String> compatibleIdps = Arrays.asList(mapper.getCompatibleProviders());
// provide the same mappers as are available for the parent provider (Keycloak-OIDC)
return compatibleIdps.contains(IdentityProviderMapper.ANY_PROVIDER)
|| compatibleIdps.contains(KeycloakOIDCIdentityProviderFactory.PROVIDER_ID);
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker.oidc;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
/**
* @author Daniel Fesenmeyer <daniel.fesenmeyer@bosch.com>
*/
public class OverwrittenMappersTestIdentityProviderFactory extends OIDCIdentityProviderFactory {
public static final String PROVIDER_ID = "overwritten-mappers-test-id-idp";
@Override
public String getName() {
return PROVIDER_ID;
}
@Override
public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new OverwrittenMappersTestIdentityProvider(session, new OIDCIdentityProviderConfig(model));
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -16,4 +16,5 @@
#
org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProviderFactory
org.keycloak.testsuite.broker.oidc.OverwrittenMappersTestIdentityProviderFactory
org.keycloak.testsuite.broker.oidc.TestKeycloakOidcIdentityProviderFactory

View file

@ -52,6 +52,7 @@ import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep;
import org.keycloak.testsuite.broker.oidc.OverwrittenMappersTestIdentityProviderFactory;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.KeyUtils;
@ -603,6 +604,27 @@ public class IdentityProviderTest extends AbstractAdminTest {
assertMapperTypes(mapperTypes, "saml-user-attribute-idp-mapper", "saml-role-idp-mapper", "saml-username-idp-mapper", "saml-advanced-role-idp-mapper", "saml-advanced-group-idp-mapper", "saml-xpath-attribute-idp-mapper");
}
@Test
public void mapperTypesCanBeOverwritten() {
String kcOidcProviderId = "keycloak-oidc";
create(createRep(kcOidcProviderId, kcOidcProviderId));
String testProviderId = OverwrittenMappersTestIdentityProviderFactory.PROVIDER_ID;
create(createRep(testProviderId, testProviderId));
/*
* in the test provider, we have overwritten the mapper types to be the same as supported by "keycloak-oidc", so
* the "keycloak-oidc" mappers are the expected mappers for the test provider
*/
IdentityProviderResource kcOidcProvider = realm.identityProviders().get(kcOidcProviderId);
Set<String> expectedMapperTypes = kcOidcProvider.getMapperTypes().keySet();
IdentityProviderResource testProvider = realm.identityProviders().get(testProviderId);
Set<String> actualMapperTypes = testProvider.getMapperTypes().keySet();
assertThat(actualMapperTypes, equalTo(expectedMapperTypes));
}
private void assertMapperTypes(Map<String, IdentityProviderMapperTypeRepresentation> mapperTypes, String ... mapperIds) {
Set<String> expected = new HashSet<>();
expected.add("hardcoded-user-session-attribute-idp-mapper");