Skip to content

Commit

Permalink
Support to define compatible mappers for (new) Identity Providers
Browse files Browse the repository at this point in the history
- Also allows to use existing mappers for custom Identity Providers without having to change those mappers

Closes keycloak#21154
  • Loading branch information
danielFesenmeyer committed Aug 31, 2023
1 parent 430af7a commit 6788658
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

Expand Down Expand Up @@ -166,4 +168,11 @@ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserMo
public IdentityProviderDataMarshaller getMarshaller() {
return new DefaultDataMarshaller();
}

@Override
public boolean isMapperSupported(IdentityProviderMapper mapper) {
List<String> compatibleIdps = Arrays.asList(mapper.getCompatibleProviders());
return compatibleIdps.contains(IdentityProviderMapper.ANY_PROVIDER)
|| compatibleIdps.contains(getConfig().getProviderId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ interface AuthenticationCallback {
Response error(String message);
}

C getConfig();


void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context);
Expand Down Expand Up @@ -132,4 +134,9 @@ interface AuthenticationCallback {
*/
IdentityProviderDataMarshaller getMarshaller();

/**
* Checks whether a mapper is supported for this Identity Provider.
*/
boolean isMapperSupported(IdentityProviderMapper mapper);

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@
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;
Expand Down Expand Up @@ -244,13 +242,14 @@ private static void updateUsersAfterProviderAliasChange(Stream<UserModel> users,
}


private IdentityProviderFactory getIdentityProviderFactory() {
private IdentityProviderFactory<?> getIdentityProviderFactory() {
String providerId = identityProviderModel.getProviderId();
return Streams.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));
}

/**
Expand All @@ -272,13 +271,17 @@ public Response export(@Parameter(description = "Format to use") @QueryParam("fo
}

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
*/
Expand All @@ -294,26 +297,22 @@ public Map<String, IdentityProviderMapperTypeRepresentation> getMapperTypes() {
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()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*/
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);
}

}
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
#

org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProviderFactory
org.keycloak.testsuite.broker.oidc.OverwrittenMappersTestIdentityProviderFactory
org.keycloak.testsuite.broker.oidc.TestKeycloakOidcIdentityProviderFactory
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
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;
Expand Down Expand Up @@ -603,6 +604,27 @@ public void testMapperTypes() {
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");
Expand Down

0 comments on commit 6788658

Please sign in to comment.