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() { public IdentityProviderDataMarshaller getMarshaller() {
return new DefaultDataMarshaller(); 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.Response;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.util.Arrays;
import java.util.List;
/** /**
* @author Pedro Igor * @author Pedro Igor
*/ */
@ -74,6 +77,8 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
Response error(String message); Response error(String message);
} }
C getConfig();
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context); void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context); void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context);
@ -132,4 +137,13 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/ */
IdentityProviderDataMarshaller getMarshaller(); 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.QueryParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; 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), return Stream.concat(session.getKeycloakSessionFactory().getProviderFactoriesStream(IdentityProvider.class),
session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.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) .map(IdentityProviderFactory.class::cast)
.findFirst() .findFirst()
.orElse(null); .orElseThrow(() -> new IllegalStateException("IDP not found by Provider ID: " + providerId));
} }
/** /**
@ -271,13 +270,17 @@ public class IdentityProviderResource {
} }
try { try {
IdentityProviderFactory factory = getIdentityProviderFactory(); return createIdentityProviderInstance().export(session.getContext().getUri(), realm, format);
return factory.create(session, identityProviderModel).export(session.getContext().getUri(), realm, format);
} catch (Exception e) { } catch (Exception e) {
throw ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND); 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 * Get mapper types for identity provider
*/ */
@ -293,13 +296,12 @@ public class IdentityProviderResource {
throw new jakarta.ws.rs.NotFoundException(); throw new jakarta.ws.rs.NotFoundException();
} }
IdentityProvider<?> identityProviderInstance = createIdentityProviderInstance();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
return sessionFactory.getProviderFactoriesStream(IdentityProviderMapper.class) return sessionFactory.getProviderFactoriesStream(IdentityProviderMapper.class)
.map(IdentityProviderMapper.class::cast) .map(IdentityProviderMapper.class::cast)
.map(mapper -> Arrays.stream(mapper.getCompatibleProviders()) .filter(identityProviderInstance::isMapperSupported)
.filter(type -> Objects.equals(IdentityProviderMapper.ANY_PROVIDER, type) || .map(mapper -> {
Objects.equals(identityProviderModel.getProviderId(), type))
.map(type -> {
IdentityProviderMapperTypeRepresentation rep = new IdentityProviderMapperTypeRepresentation(); IdentityProviderMapperTypeRepresentation rep = new IdentityProviderMapperTypeRepresentation();
rep.setId(mapper.getId()); rep.setId(mapper.getId());
rep.setCategory(mapper.getDisplayCategory()); rep.setCategory(mapper.getDisplayCategory());
@ -310,9 +312,6 @@ public class IdentityProviderResource {
.collect(Collectors.toList())); .collect(Collectors.toList()));
return rep; return rep;
}) })
.findFirst()
.orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toMap(IdentityProviderMapperTypeRepresentation::getId, Function.identity())); .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.LegacyIdIdentityProviderFactory
org.keycloak.testsuite.broker.oidc.OverwrittenMappersTestIdentityProviderFactory
org.keycloak.testsuite.broker.oidc.TestKeycloakOidcIdentityProviderFactory 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.saml.processing.core.util.XMLSignatureUtil;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep; import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep;
import org.keycloak.testsuite.broker.oidc.OverwrittenMappersTestIdentityProviderFactory;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.KeyUtils; 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"); 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) { private void assertMapperTypes(Map<String, IdentityProviderMapperTypeRepresentation> mapperTypes, String ... mapperIds) {
Set<String> expected = new HashSet<>(); Set<String> expected = new HashSet<>();
expected.add("hardcoded-user-session-attribute-idp-mapper"); expected.add("hardcoded-user-session-attribute-idp-mapper");